From 0b500e7e15071d182a7530c9d60a9941e9dbe5d1 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 22:08:45 +0000 Subject: [PATCH 1/8] Test that an update applied in memory refreshes the engine once DataUpdateService_CheckForUpdate_InMemory_RefreshesOnce runs a check for a data file with an update URL and no file path, so the downloaded data is applied in memory. It counts every refreshData call on the engine and expects exactly one, the call with the downloaded data. --- .../services/DataUpdateServiceTests.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java b/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java index 52176409..0b6901c9 100644 --- a/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java +++ b/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java @@ -45,6 +45,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatcher; import org.mockito.internal.matchers.LessOrEqual; +import org.mockito.invocation.Invocation; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.slf4j.LoggerFactory; @@ -61,6 +62,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; @@ -72,6 +74,7 @@ import static fiftyone.pipeline.engines.services.DataUpdateService.AutoUpdateStatus.AUTO_UPDATE_HTTPS_ERR; import static fiftyone.pipeline.engines.services.DataUpdateService.AutoUpdateStatus.AUTO_UPDATE_NOT_NEEDED; +import static fiftyone.pipeline.engines.services.DataUpdateService.AutoUpdateStatus.AUTO_UPDATE_SUCCESS; import static org.junit.Assert.*; import static org.junit.Assume.assumeFalse; import static org.mockito.Mockito.*; @@ -1229,6 +1232,57 @@ public boolean matches(byte[] argument) { verify(futureFactory, times(1)).schedule(any(Runnable.class), anyLong()); } + /** + * A check that finds an update for a data file with no file path + * applies it in memory, so it refreshes the engine once, with the + * downloaded data, and does not refresh it again. + */ + @Test + public void DataUpdateService_CheckForUpdate_InMemory_RefreshesOnce() + throws IOException { + // Arrange + configureNoFileSystem(); + when(httpClientConnection.getResponseCode()) + .thenReturn(HttpURLConnection.HTTP_OK); + when(httpClientConnection.getResponseMessage()).thenReturn(null); + when(httpClientConnection.getHeaderField("Content-MD5")) + .thenReturn("08527dcbdd437e7fa6c084423d06dba6"); + when(httpClientConnection.getInputStream()).thenReturn( + DataUpdateServiceTests.class.getClassLoader() + .getResourceAsStream("file.gz")); + + OnPremiseAspectEngine engine = + mock(OnPremiseAspectEngine.class); + DataFileConfigurationDefault config = new DataFileConfigurationDefault(); + config.setDataUpdateUrl("http://www.test.com"); + config.setVerifyMd5(true); + config.setDecompressContent(true); + config.setVerifyModifiedSince(false); + + AspectEngineDataFileDefault file = new AspectEngineDataFileDefault(); + file.setEngine(engine); + file.setConfiguration(config); + when(engine.getDataFileMetaData()).thenReturn(file); + + // Act + DataUpdateService.AutoUpdateStatus status = + dataUpdate.checkForUpdate(engine); + + // Assert + assertEquals(AUTO_UPDATE_SUCCESS, status); + List refreshes = new ArrayList<>(); + for (Invocation invocation : mockingDetails(engine).getInvocations()) { + if (invocation.getMethod().getName().equals("refreshData")) { + refreshes.add(invocation.getMethod().toGenericString()); + } + } + assertEquals( + "The engine was not refreshed exactly once: " + refreshes, + 1, + refreshes.size()); + verify(engine, times(1)).refreshData(any(), any(byte[].class)); + } + /** From 66a57048bae5d20380e7799d27213e71344ac43f Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 22:09:15 +0000 Subject: [PATCH 2/8] Refresh the engine once when an update is applied in memory When a data file has no file path, checkForUpdateFromUrl applies the downloaded data to the engine in memory, then returned AUTO_UPDATE_IN_PROGRESS. checkForUpdate took that to mean the engine had not been refreshed and called updatedFileAvailable, which refreshed the engine a second time through refreshData(String), the method that reloads from a data file. The in-memory path now returns AUTO_UPDATE_SUCCESS once the engine, or the configuration where there is no engine yet, holds the new data, as .NET's DataUpdateService does. checkForUpdate then clears the timer as updatedFileAvailable did, so the next check is registered as before. --- .../engines/services/DataUpdateServiceDefault.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pipeline/pipeline.engines/src/main/java/fiftyone/pipeline/engines/services/DataUpdateServiceDefault.java b/pipeline/pipeline.engines/src/main/java/fiftyone/pipeline/engines/services/DataUpdateServiceDefault.java index 007b2121..5169a494 100644 --- a/pipeline/pipeline.engines/src/main/java/fiftyone/pipeline/engines/services/DataUpdateServiceDefault.java +++ b/pipeline/pipeline.engines/src/main/java/fiftyone/pipeline/engines/services/DataUpdateServiceDefault.java @@ -226,6 +226,15 @@ private AutoUpdateStatus checkForUpdate(Object state, boolean manualUpdate) { // yet been refreshed. logger.debug("Calling updatedFileAvailable"); result = updatedFileAvailable(dataFile); + } else if (result == AUTO_UPDATE_SUCCESS) { + // The update was applied in memory. Clear the timer, as + // updatedFileAvailable does, so a new one is registered. + synchronized (futureLock) { + if (dataFile.getFuture() != null) { + dataFile.getFuture().cancel(true); + dataFile.setFuture(null); + } + } } else { // No update available. // If this was a manual call to update then do nothing. @@ -805,7 +814,8 @@ private AutoUpdateStatus checkForUpdateFromUrl(AspectEngineDataFile dataFile) { dataFile.getConfiguration().setData(reader.toByteArray()); } } - return memoryDownloadResult; + // The engine, or the configuration, now holds the new data. + return AUTO_UPDATE_SUCCESS; } catch (Exception ex) { logger.error("An error occurred when applying a data update to engine '{}'" , Objects.nonNull(dataFile.getEngine())? From d99c9b7fceff00ab0c2d4184c018c92cebe6a3f2 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 22:40:19 +0000 Subject: [PATCH 3/8] Wait for the watcher's update in the watcher test, not the polling timer's check With a one second polling interval the timer's check can complete first, finding no update because the watcher is enabled, and release the test before the watcher has refreshed the engine. The failed run took 1.008 seconds. The flag is now released only by a completion whose status is AUTO_UPDATE_SUCCESS, which only the watcher's update gives. --- .../engines/services/DataUpdateServiceTests.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java b/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java index 0b6901c9..c6322fc2 100644 --- a/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java +++ b/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java @@ -418,14 +418,17 @@ public Object answer(InvocationOnMock invocationOnMock) throws Throwable { } }); - // Configure a flag to be set when processing - // is complete. + // Configure a flag to be set when the watcher has applied the + // update. The polling timer's own check also completes, with no + // update found, and must not release the flag. final Semaphore completeFlag = new Semaphore(1); completeFlag.acquire(); dataUpdate.onUpdateComplete(new OnUpdateComplete() { @Override public void call(Object sender, DataUpdateService.DataUpdateCompleteArgs args) { - completeFlag.release(); + if (args.getStatus() == AUTO_UPDATE_SUCCESS) { + completeFlag.release(); + } } }); From 6ff3192225cae116dfcc3d9207cf5d1b00951a5a Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 23:54:08 +0000 Subject: [PATCH 4/8] Test that a manual update sets the next update timer again The data updates page of the specification on rc says the automatic update master switch "only governs *automatic* activity - it does not disable *Programmatic update*". A programmatic update processes a newer file "as per newFile above", and the steps of newFileAvailable are: - pause file watcher, if any - pause update polling, if any - load file - resume polling, if any - resume file watcher, if any - trigger onUpdateCallbacks Three tests cover a manual check on a data file registered with the service, for each setting of automatic updates. DataUpdateService_CheckForUpdate_FileUpdated_TimerSetAgain applies an update from the data file, and DataUpdateService_CheckForUpdate_InMemory_TimerSetAgain applies one downloaded in memory. Each expects a new timer where automatic updates are on. Both are written to fail where automatic updates are on, because the manual update clears the timer and only a check the timer started registers the data file again. DataUpdateService_CheckForUpdate_NoUpdate_TimerKept checks that a manual check finding no update leaves the existing timer as it was. It passes before and after the change. --- .../services/DataUpdateServiceTests.java | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java b/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java index c6322fc2..f34adfc5 100644 --- a/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java +++ b/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java @@ -1286,6 +1286,141 @@ public void DataUpdateService_CheckForUpdate_InMemory_RefreshesOnce() verify(engine, times(1)).refreshData(any(), any(byte[].class)); } + /** + * A manual check that applies an update from the data file to a data + * file registered for automatic updates sets the next update timer, as + * a check the timer started does. + */ + @Test + public void DataUpdateService_CheckForUpdate_FileUpdated_TimerSetAgain() { + // Arrange + OnPremiseAspectEngine engine = + mock(OnPremiseAspectEngine.class); + DataFileConfigurationDefault config = new DataFileConfigurationDefault(); + config.setAutomaticUpdatesEnabled(autoUpdateEnabled); + config.setFileSystemWatcherEnabled(false); + + AspectEngineDataFileDefault file = new AspectEngineDataFileDefault(); + file.setEngine(engine); + file.setConfiguration(config); + when(engine.getDataFileMetaData()).thenReturn(file); + configureFileUpdate(engine); + + dataUpdate.registerDataFile(file); + int timers = autoUpdateEnabled ? 1 : 0; + verify(futureFactory, times(timers)).schedule( + any(Runnable.class), + anyLong()); + + // Act + DataUpdateService.AutoUpdateStatus status = + dataUpdate.checkForUpdate(engine); + + // Assert + assertEquals(AUTO_UPDATE_SUCCESS, status); + verify(engine, times(1)).refreshData((String) any()); + verify(futureFactory, times(2 * timers)).schedule( + any(Runnable.class), + anyLong()); + assertEquals( + "The next update timer was not set after the manual update.", + autoUpdateEnabled, + file.getFuture() != null); + } + + /** + * A manual check that applies an update in memory to a data file + * registered for automatic updates sets the next update timer, as a + * check the timer started does. + */ + @Test + public void DataUpdateService_CheckForUpdate_InMemory_TimerSetAgain() + throws IOException { + // Arrange + configureNoFileSystem(); + when(httpClientConnection.getResponseCode()) + .thenReturn(HttpURLConnection.HTTP_OK); + when(httpClientConnection.getResponseMessage()).thenReturn(null); + when(httpClientConnection.getHeaderField("Content-MD5")) + .thenReturn("08527dcbdd437e7fa6c084423d06dba6"); + when(httpClientConnection.getInputStream()).thenReturn( + DataUpdateServiceTests.class.getClassLoader() + .getResourceAsStream("file.gz")); + + OnPremiseAspectEngine engine = + mock(OnPremiseAspectEngine.class); + DataFileConfigurationDefault config = new DataFileConfigurationDefault(); + config.setAutomaticUpdatesEnabled(autoUpdateEnabled); + config.setDataUpdateUrl("http://www.test.com"); + config.setVerifyMd5(true); + config.setDecompressContent(true); + config.setVerifyModifiedSince(false); + + AspectEngineDataFileDefault file = new AspectEngineDataFileDefault(); + file.setEngine(engine); + file.setConfiguration(config); + when(engine.getDataFileMetaData()).thenReturn(file); + + dataUpdate.registerDataFile(file); + int timers = autoUpdateEnabled ? 1 : 0; + verify(futureFactory, times(timers)).schedule( + any(Runnable.class), + anyLong()); + + // Act + DataUpdateService.AutoUpdateStatus status = + dataUpdate.checkForUpdate(engine); + + // Assert + assertEquals(AUTO_UPDATE_SUCCESS, status); + verify(futureFactory, times(2 * timers)).schedule( + any(Runnable.class), + anyLong()); + assertEquals( + "The next update timer was not set after the manual update.", + autoUpdateEnabled, + file.getFuture() != null); + } + + /** + * A manual check that finds no update leaves the timer of a data file + * registered for automatic updates as it was. + */ + @Test + public void DataUpdateService_CheckForUpdate_NoUpdate_TimerKept() + throws IOException { + // Arrange + configureNoFileSystem(); + configureHttpNoUpdateAvailable(); + + OnPremiseAspectEngine engine = + mock(OnPremiseAspectEngine.class); + DataFileConfigurationDefault config = new DataFileConfigurationDefault(); + config.setAutomaticUpdatesEnabled(autoUpdateEnabled); + config.setDataUpdateUrl("http://www.test.com"); + config.setVerifyModifiedSince(false); + + AspectEngineDataFileDefault file = new AspectEngineDataFileDefault(); + file.setEngine(engine); + file.setConfiguration(config); + when(engine.getDataFileMetaData()).thenReturn(file); + + dataUpdate.registerDataFile(file); + Object timer = file.getFuture(); + + // Act + DataUpdateService.AutoUpdateStatus status = + dataUpdate.checkForUpdate(engine); + + // Assert + assertEquals(AUTO_UPDATE_NOT_NEEDED, status); + verify(engine, never()).refreshData(anyString()); + verify(futureFactory, times(autoUpdateEnabled ? 1 : 0)).schedule( + any(Runnable.class), + anyLong()); + assertSame(timer, file.getFuture()); + } + /** From 399afbb11c542c31618f37d559fe4703c51d33c7 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 23:54:47 +0000 Subject: [PATCH 5/8] Set the next update timer after a manual update, as the specification says The data updates page of the specification on rc says the automatic update master switch "only governs *automatic* activity - it does not disable *Programmatic update*". Under Programmatic update, a newer file is processed "as per newFile above", and the steps of newFileAvailable include "resume polling, if any" and "resume file watcher, if any". checkForUpdate registered the data file again only after a check the timer started. A manual update cleared the timer, through updatedFileAvailable or the in-memory success branch, and set no new one, so polling stopped. checkForUpdate now also registers the data file again after a manual check that applied an update, where the data file is registered with the service, so registerDataFile sets the next timer when automatic updates are on, as it does after an automatic check. A manual check that finds no update still leaves the timer as it was, and a data file that was never registered is not registered by a manual check. --- .../pipeline/engines/services/DataUpdateServiceDefault.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pipeline/pipeline.engines/src/main/java/fiftyone/pipeline/engines/services/DataUpdateServiceDefault.java b/pipeline/pipeline.engines/src/main/java/fiftyone/pipeline/engines/services/DataUpdateServiceDefault.java index 5169a494..ca72d4e4 100644 --- a/pipeline/pipeline.engines/src/main/java/fiftyone/pipeline/engines/services/DataUpdateServiceDefault.java +++ b/pipeline/pipeline.engines/src/main/java/fiftyone/pipeline/engines/services/DataUpdateServiceDefault.java @@ -258,9 +258,13 @@ public void run() { } } } - if (manualUpdate == false) { + if (manualUpdate == false || + (result == AUTO_UPDATE_SUCCESS && + dataFile.getIsRegistered())) { // Re-register the engine with the data update service // so it knows when next set of data should be available. + // A manual update of a registered data file does this too, + // so polling resumes after it. registerDataFile(dataFile); } From 4215118a7376e0e722819e8b2a63624bc7f5526d Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 23:55:35 +0000 Subject: [PATCH 6/8] Check that a manual update leaves one update timer, not two Both TimerSetAgain tests now keep the timer pending before the manual update and check, where automatic updates are on, that it was cancelled and that the data file holds a different one. With the existing check that exactly one more timer was scheduled, this shows a manual update with a timer pending leaves one live timer. The pending timer is cancelled and cleared under futureLock, by updatedFileAvailable for the file path or by the in-memory success branch, before registerDataFile schedules a new one, and registerDataFile schedules only while the future is null under the same lock. --- .../services/DataUpdateServiceTests.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java b/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java index f34adfc5..abc00fa1 100644 --- a/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java +++ b/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java @@ -1312,6 +1312,8 @@ public void DataUpdateService_CheckForUpdate_FileUpdated_TimerSetAgain() { any(Runnable.class), anyLong()); + ScheduledFuture pending = file.getFuture(); + // Act DataUpdateService.AutoUpdateStatus status = dataUpdate.checkForUpdate(engine); @@ -1326,6 +1328,14 @@ public void DataUpdateService_CheckForUpdate_FileUpdated_TimerSetAgain() { "The next update timer was not set after the manual update.", autoUpdateEnabled, file.getFuture() != null); + + if (autoUpdateEnabled) { + // The pending timer is cancelled, so only the new one is left. + assertTrue( + "The timer pending before the manual update was not cancelled.", + pending.isCancelled()); + assertNotSame(pending, file.getFuture()); + } } /** @@ -1367,6 +1377,8 @@ public void DataUpdateService_CheckForUpdate_InMemory_TimerSetAgain() any(Runnable.class), anyLong()); + ScheduledFuture pending = file.getFuture(); + // Act DataUpdateService.AutoUpdateStatus status = dataUpdate.checkForUpdate(engine); @@ -1380,6 +1392,14 @@ public void DataUpdateService_CheckForUpdate_InMemory_TimerSetAgain() "The next update timer was not set after the manual update.", autoUpdateEnabled, file.getFuture() != null); + + if (autoUpdateEnabled) { + // The pending timer is cancelled, so only the new one is left. + assertTrue( + "The timer pending before the manual update was not cancelled.", + pending.isCancelled()); + assertNotSame(pending, file.getFuture()); + } } /** From 7d0b6e2d9d5b09e7328c17d2c8f1bc150bd0e2c3 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Tue, 10 Nov 2026 00:23:06 +0000 Subject: [PATCH 7/8] Test that a failed manual refresh keeps polling, and that unregistering unregisters DataUpdateService_CheckForUpdate_FileRefreshFails_TimerSetAgain makes a manual check whose refresh from the data file fails, on a data file registered for automatic updates, and expects the next update timer to be set where automatic updates are on. The newFileAvailable steps in the data updates page of the specification say "resume polling, if any", with no exception for a failed load. DataUpdateService_Unregister_NoLongerRegistered expects a data file to be no longer registered after unregisterDataFile, so a manual update of it sets no timer. Both are written to fail on the previous commit. The first fails where automatic updates are on. --- .../services/DataUpdateServiceTests.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java b/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java index abc00fa1..4ed689fc 100644 --- a/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java +++ b/pipeline/pipeline.engines/src/test/java/fiftyone/pipeline/engines/services/DataUpdateServiceTests.java @@ -1338,6 +1338,91 @@ public void DataUpdateService_CheckForUpdate_FileUpdated_TimerSetAgain() { } } + /** + * A manual check whose refresh from the data file fails still leaves + * the next update timer set, as a check the timer started does, so + * polling resumes. + */ + @Test + public void DataUpdateService_CheckForUpdate_FileRefreshFails_TimerSetAgain() { + // updatedFileAvailable warns on each of its 10 tries. + ignoreWranings = 10; + + // Arrange + OnPremiseAspectEngine engine = + mock(OnPremiseAspectEngine.class); + doThrow(new IllegalStateException("The refresh failed.")) + .when(engine).refreshData((String) any()); + DataFileConfigurationDefault config = new DataFileConfigurationDefault(); + config.setAutomaticUpdatesEnabled(autoUpdateEnabled); + config.setFileSystemWatcherEnabled(false); + + AspectEngineDataFileDefault file = new AspectEngineDataFileDefault(); + file.setEngine(engine); + file.setConfiguration(config); + when(engine.getDataFileMetaData()).thenReturn(file); + configureFileUpdate(engine); + + dataUpdate.registerDataFile(file); + int timers = autoUpdateEnabled ? 1 : 0; + ScheduledFuture pending = file.getFuture(); + + // Act + DataUpdateService.AutoUpdateStatus status = + dataUpdate.checkForUpdate(engine); + + // Assert + assertEquals( + DataUpdateService.AutoUpdateStatus.AUTO_UPDATE_REFRESH_FAILED, + status); + verify(futureFactory, times(2 * timers)).schedule( + any(Runnable.class), + anyLong()); + assertEquals( + "The next update timer was not set after the failed refresh.", + autoUpdateEnabled, + file.getFuture() != null); + if (autoUpdateEnabled) { + assertTrue(pending.isCancelled()); + assertNotSame(pending, file.getFuture()); + } + } + + /** + * A data file that has been unregistered is no longer registered, so a + * manual update of it sets no update timer. + */ + @Test + public void DataUpdateService_Unregister_NoLongerRegistered() { + // Arrange + OnPremiseAspectEngine engine = + mock(OnPremiseAspectEngine.class); + DataFileConfigurationDefault config = new DataFileConfigurationDefault(); + config.setAutomaticUpdatesEnabled(autoUpdateEnabled); + config.setFileSystemWatcherEnabled(false); + + AspectEngineDataFileDefault file = new AspectEngineDataFileDefault(); + file.setEngine(engine); + file.setConfiguration(config); + when(engine.getDataFileMetaData()).thenReturn(file); + configureFileUpdate(engine); + dataUpdate.registerDataFile(file); + int timers = autoUpdateEnabled ? 1 : 0; + + // Act + dataUpdate.unregisterDataFile(file); + DataUpdateService.AutoUpdateStatus status = + dataUpdate.checkForUpdate(engine); + + // Assert + assertFalse(file.getIsRegistered()); + assertEquals(AUTO_UPDATE_SUCCESS, status); + verify(futureFactory, times(timers)).schedule( + any(Runnable.class), + anyLong()); + assertNull(file.getFuture()); + } + /** * A manual check that applies an update in memory to a data file * registered for automatic updates sets the next update timer, as a From 3e51394327a7c289af295eb9419b7bf935662132 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Tue, 10 Nov 2026 00:23:23 +0000 Subject: [PATCH 8/8] Resume polling after a failed manual refresh, and clear the registration on unregister updatedFileAvailable clears the update timer before it refreshes the engine, so after a manual check whose refresh failed polling stopped, where an automatic check registers the data file again. checkForUpdate now also registers a registered data file again after a manual check that returned AUTO_UPDATE_REFRESH_FAILED. A manual failure that did not clear the timer, such as one in memory, keeps the timer it had, because registerDataFile schedules only while there is none. So a manual check leaves one timer whether it applied an update, failed or found none. unregisterDataFile now sets the data file's update service to null, so getIsRegistered is false after it and a later manual update does not register the data file again. --- .../engines/services/DataUpdateServiceDefault.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pipeline/pipeline.engines/src/main/java/fiftyone/pipeline/engines/services/DataUpdateServiceDefault.java b/pipeline/pipeline.engines/src/main/java/fiftyone/pipeline/engines/services/DataUpdateServiceDefault.java index ca72d4e4..f366dbfb 100644 --- a/pipeline/pipeline.engines/src/main/java/fiftyone/pipeline/engines/services/DataUpdateServiceDefault.java +++ b/pipeline/pipeline.engines/src/main/java/fiftyone/pipeline/engines/services/DataUpdateServiceDefault.java @@ -259,12 +259,14 @@ public void run() { } } if (manualUpdate == false || - (result == AUTO_UPDATE_SUCCESS && + ((result == AUTO_UPDATE_SUCCESS || + result == AUTO_UPDATE_REFRESH_FAILED) && dataFile.getIsRegistered())) { // Re-register the engine with the data update service // so it knows when next set of data should be available. // A manual update of a registered data file does this too, - // so polling resumes after it. + // whether or not the refresh worked, as the refresh clears + // the timer, so polling resumes after it. registerDataFile(dataFile); } @@ -578,6 +580,9 @@ public void unregisterDataFile(AspectEngineDataFile dataFile) { } } } + if (dataFile != null) { + dataFile.setDataUpdateService(null); + } } /**