This is an automated email from the ASF dual-hosted git repository.

jongyoul pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new 7bb88cabfd [ZEPPELIN-6453] Handle secondary NotebookRepo remove 
failures consistently
7bb88cabfd is described below

commit 7bb88cabfdb6bc338955ceba225ae54b1a5a8e3f
Author: DONGHOON LEE <[email protected]>
AuthorDate: Sun Jul 12 13:25:28 2026 +0900

    [ZEPPELIN-6453] Handle secondary NotebookRepo remove failures consistently
    
    ### What is this PR for?
    In `NotebookRepoSync`, secondary storage failures were already handled for 
`save()` and `move()`, but `remove()` still used a generic `for` loop with a 
`TODO` comment for handling secondary remove failures.
    
    This PR makes the `remove()` failure handling consistent with the existing 
`save()` and `move()` behavior:
    * **Primary Storage (`getRepo(0)`)**: A failure during removal stops the 
operation and propagates the `IOException`.
    * **Secondary Storage (`getRepo(1)`)**: A failure during removal is caught 
and explicitly logged, preventing silent inconsistencies without interrupting 
the overall operation.
    
    To achieve this, the `for` loop in `remove(String noteId, String notePath, 
AuthenticationInfo subject)` was replaced with explicit `getRepo(0)` and 
`getRepo(1)` calls, and the outstanding `TODO` was removed.
    
    ### What type of PR is it?
    Improvement
    
    ### What is the Jira issue?
    * https://issues.apache.org/jira/browse/ZEPPELIN-6453
    
    ### How should this be tested?
    Tested via `NotebookRepoSyncTest` using Mockito to explicitly verify the 
primary vs. secondary failure behaviors:
    * `testRemoveSucceedsWhenSecondaryRepoFails`: Verifies that an 
`IOException` from the secondary repo is caught and does not stop the removal 
process.
    * `testRemoveFailsWhenPrimaryRepoFails`: Verifies that an `IOException` 
from the primary repo halts the operation and propagates the exception.
    * All existing tests in `NotebookRepoSyncTest` continue to pass.
    
    ### Screenshots (if appropriate)
    N/A
    
    ### Questions:
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    Closes #5289 from move-hoon/ZEPPELIN-6453-remove-secondary-failure.
    
    Signed-off-by: Jongyoul Lee <[email protected]>
---
 .../zeppelin/notebook/repo/NotebookRepoSync.java   | 11 ++--
 .../notebook/repo/NotebookRepoSyncTest.java        | 61 ++++++++++++++++++++++
 2 files changed, 69 insertions(+), 3 deletions(-)

diff --git 
a/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java
 
b/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java
index cbc2263c71..eb5b1e37e5 100644
--- 
a/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java
+++ 
b/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java
@@ -208,10 +208,15 @@ public class NotebookRepoSync implements 
NotebookRepoWithVersionControl {
 
   @Override
   public void remove(String noteId, String notePath, AuthenticationInfo 
subject) throws IOException {
-    for (NotebookRepo repo : repos) {
-      repo.remove(noteId, notePath, subject);
+    getRepo(0).remove(noteId, notePath, subject);
+    if (getRepoCount() > 1) {
+      try {
+        getRepo(1).remove(noteId, notePath, subject);
+      }
+      catch (IOException e) {
+        LOGGER.info("{}: Failed to remove from secondary storage", 
e.getMessage());
+      }
     }
-    /* TODO(khalid): handle case when removing from secondary storage fails */
   }
 
   @Override
diff --git 
a/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java
 
b/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java
index 539a160dcb..3a90e4f603 100644
--- 
a/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java
+++ 
b/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java
@@ -18,12 +18,20 @@
 package org.apache.zeppelin.notebook.repo;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
@@ -434,4 +442,57 @@ class NotebookRepoSyncTest {
     assertEquals(0, authorizationService.getRunners(noteId).size());
     assertEquals(0, authorizationService.getWriters(noteId).size());
   }
+
+  @Test
+  void testRemoveSucceedsWhenSecondaryRepoFails() throws IOException {
+    NotebookRepo primaryRepo = mock(NotebookRepo.class);
+    NotebookRepo secondaryRepo = mock(NotebookRepo.class);
+    doThrow(new IOException("secondary remove failed"))
+        .when(secondaryRepo).remove("noteId", "notePath", anonymous);
+
+    try (NotebookRepoSync repoSync = newSyncWithRepos(primaryRepo, 
secondaryRepo)) {
+      // secondary storage failure must not fail the whole operation
+      repoSync.remove("noteId", "notePath", anonymous);
+
+      verify(primaryRepo, times(1)).remove("noteId", "notePath", anonymous);
+      verify(secondaryRepo, times(1)).remove("noteId", "notePath", anonymous);
+    }
+  }
+
+  @Test
+  void testRemoveFailsWhenPrimaryRepoFails() throws IOException {
+    NotebookRepo primaryRepo = mock(NotebookRepo.class);
+    NotebookRepo secondaryRepo = mock(NotebookRepo.class);
+    doThrow(new IOException("primary remove failed"))
+        .when(primaryRepo).remove("noteId", "notePath", anonymous);
+
+    try (NotebookRepoSync repoSync = newSyncWithRepos(primaryRepo, 
secondaryRepo)) {
+      // primary storage failure must stop the operation and propagate the 
exception
+      assertThrows(IOException.class,
+          () -> repoSync.remove("noteId", "notePath", anonymous));
+
+      verify(primaryRepo, times(1)).remove("noteId", "notePath", anonymous);
+      verify(secondaryRepo, never()).remove("noteId", "notePath", anonymous);
+    }
+  }
+
+  /**
+   * Builds a NotebookRepoSync backed by the two given repos, injected through 
a mocked
+   * PluginManager so the real init() path is exercised without touching 
internal fields.
+   */
+  private NotebookRepoSync newSyncWithRepos(NotebookRepo primaryRepo, 
NotebookRepo secondaryRepo)
+      throws IOException {
+    PluginManager mockPluginManager = mock(PluginManager.class);
+    
when(mockPluginManager.loadNotebookRepo("primaryRepo")).thenReturn(primaryRepo);
+    
when(mockPluginManager.loadNotebookRepo("secondaryRepo")).thenReturn(secondaryRepo);
+    // list() is queried during the init-time anonymous sync; keep it a no-op
+    when(primaryRepo.list(any())).thenReturn(new HashMap<>());
+    when(secondaryRepo.list(any())).thenReturn(new HashMap<>());
+
+    zConf.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_STORAGE.getVarName(),
+        "primaryRepo,secondaryRepo");
+    NotebookRepoSync repoSync = new NotebookRepoSync(mockPluginManager);
+    repoSync.init(zConf, noteParser);
+    return repoSync;
+  }
 }

Reply via email to