You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by mi...@apache.org on 2025/12/19 10:52:51 UTC

(jackrabbit-oak) branch trunk updated: OAK-12039 test that verifies that lease renewal in AzureRepositoryLockV8 correctly handles client timeout (#2667)

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

miroslav pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 6697d706d9 OAK-12039 test that verifies that lease renewal in AzureRepositoryLockV8 correctly handles client timeout (#2667)
6697d706d9 is described below

commit 6697d706d92f6928adc9768a3b8f2cb6f070f310
Author: Miroslav Smiljanic <sm...@gmail.com>
AuthorDate: Fri Dec 19 11:52:47 2025 +0100

    OAK-12039 test that verifies that lease renewal in AzureRepositoryLockV8 correctly handles client timeout (#2667)
    
    Co-authored-by: smiroslav <mi...@apache.com>
---
 oak-segment-azure/pom.xml                          | 12 +++
 .../segment/azure/v8/AzureRepositoryLockV8.java    |  4 +-
 .../azure/v8/AzureRepositoryLockV8Test.java        | 91 +++++++++++++++++++++-
 3 files changed, 104 insertions(+), 3 deletions(-)

diff --git a/oak-segment-azure/pom.xml b/oak-segment-azure/pom.xml
index fccb98a93d..a71670ae1a 100644
--- a/oak-segment-azure/pom.xml
+++ b/oak-segment-azure/pom.xml
@@ -451,6 +451,18 @@
             <artifactId>system-rules</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.wiremock</groupId>
+            <artifactId>wiremock</artifactId>
+            <version>3.9.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.awaitility</groupId>
+            <artifactId>awaitility</artifactId>
+            <version>4.2.1</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
 </project>
diff --git a/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/v8/AzureRepositoryLockV8.java b/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/v8/AzureRepositoryLockV8.java
index 9f58115125..d066f1dc96 100644
--- a/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/v8/AzureRepositoryLockV8.java
+++ b/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/v8/AzureRepositoryLockV8.java
@@ -37,7 +37,9 @@ public class AzureRepositoryLockV8 implements RepositoryLock {
     private static final Logger log = LoggerFactory.getLogger(AzureRepositoryLockV8.class);
 
     private static final int TIMEOUT_SEC = Integer.getInteger("oak.segment.azure.lock.timeout", 0);
-    private static final Integer LEASE_RENEWAL_TIMEOUT_MS = 5000;
+
+    public static final String LEASE_RENEWAL_TIMEOUT_PROP = "oak.segment.azure.lock.leaseRenewalTimeoutInMs";
+    private static final int LEASE_RENEWAL_TIMEOUT_MS = Integer.getInteger(LEASE_RENEWAL_TIMEOUT_PROP, 5000);
 
     public static final String LEASE_DURATION_PROP = "oak.segment.azure.lock.leaseDurationInSec";
     private final int leaseDuration = Integer.getInteger(LEASE_DURATION_PROP, 60);
diff --git a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/v8/AzureRepositoryLockV8Test.java b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/v8/AzureRepositoryLockV8Test.java
index d645a1743f..59cc74dc43 100644
--- a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/v8/AzureRepositoryLockV8Test.java
+++ b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/v8/AzureRepositoryLockV8Test.java
@@ -18,11 +18,15 @@
  */
 package org.apache.jackrabbit.oak.segment.azure.v8;
 
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import com.github.tomakehurst.wiremock.stubbing.Scenario;
+import com.microsoft.azure.storage.CloudStorageAccount;
 import com.microsoft.azure.storage.StorageErrorCodeStrings;
 import com.microsoft.azure.storage.StorageException;
+import com.microsoft.azure.storage.blob.CloudBlobClient;
 import com.microsoft.azure.storage.blob.CloudBlobContainer;
 import com.microsoft.azure.storage.blob.CloudBlockBlob;
-
 import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzuriteDockerRule;
 import org.apache.jackrabbit.oak.segment.remote.WriteAccessController;
 import org.apache.jackrabbit.oak.segment.spi.persistence.RepositoryLock;
@@ -40,7 +44,10 @@ import java.net.URISyntaxException;
 import java.security.InvalidKeyException;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
 
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.awaitility.Awaitility.await;
 import static org.junit.Assert.*;
 
 public class AzureRepositoryLockV8Test {
@@ -49,6 +56,7 @@ public class AzureRepositoryLockV8Test {
     public static final String LEASE_DURATION = "15";
     public static final String RENEWAL_INTERVAL = "3";
     public static final String TIME_TO_WAIT_BEFORE_BLOCK = "9";
+    public static final String LEASE_RENEWAL_TIMEOUT = "1000";
 
     @ClassRule
     public static AzuriteDockerRule azurite = new AzuriteDockerRule();
@@ -63,7 +71,8 @@ public class AzureRepositoryLockV8Test {
     @Rule
     public final ProvideSystemProperty systemPropertyRule = new ProvideSystemProperty(AzureRepositoryLockV8.LEASE_DURATION_PROP, LEASE_DURATION)
             .and(AzureRepositoryLockV8.RENEWAL_INTERVAL_PROP, RENEWAL_INTERVAL)
-            .and(AzureRepositoryLockV8.TIME_TO_WAIT_BEFORE_WRITE_BLOCK_PROP, TIME_TO_WAIT_BEFORE_BLOCK);
+            .and(AzureRepositoryLockV8.TIME_TO_WAIT_BEFORE_WRITE_BLOCK_PROP, TIME_TO_WAIT_BEFORE_BLOCK)
+            .and(AzureRepositoryLockV8.LEASE_RENEWAL_TIMEOUT_PROP, LEASE_RENEWAL_TIMEOUT);
 
     @Test
     public void testFailingLock() throws URISyntaxException, IOException, StorageException {
@@ -168,4 +177,82 @@ public class AzureRepositoryLockV8Test {
 
         Mockito.doCallRealMethod().when(blobMocked).renewLease(Mockito.any(), Mockito.any(), Mockito.any());
     }
+
+    @Test
+    public void testClientSideTimeoutExceptionIsRecoverable() throws Exception {
+
+        // Start WireMock as a proxy
+        WireMockServer wireMockServer = new WireMockServer(WireMockConfiguration.options()
+                .dynamicPort());
+        wireMockServer.start();
+
+        try {
+            int wireMockPort = wireMockServer.port();
+            int azuritePort = azurite.getMappedPort();
+            String azuriteUrl = "http://127.0.0.1:" + azuritePort;
+
+            // Configure WireMock to proxy all requests to Azurite by default
+            wireMockServer.stubFor(any(anyUrl())
+                    .willReturn(aResponse().proxiedFrom(azuriteUrl)));
+
+            // Use WireMock scenarios to delay only the first 2 lease renewal requests
+            // Scenario: Started -> FirstTimeout -> SecondTimeout -> Success
+            String scenarioName = "LeaseRenewalTimeout";
+
+            // First renewal request: delay and transition to SecondTimeout state
+            wireMockServer.stubFor(put(urlPathMatching(".*/oak/repo\\.lock"))
+                    .withQueryParam("comp", equalTo("lease"))
+                    .inScenario(scenarioName)
+                    .whenScenarioStateIs(Scenario.STARTED)
+                    .willReturn(aResponse().proxiedFrom(azuriteUrl).withFixedDelay(2000))
+                    .willSetStateTo("SecondTimeout"));
+
+            // Second renewal request: delay and transition to Success state
+            wireMockServer.stubFor(put(urlPathMatching(".*/oak/repo\\.lock"))
+                    .withQueryParam("comp", equalTo("lease"))
+                    .inScenario(scenarioName)
+                    .whenScenarioStateIs("SecondTimeout")
+                    .willReturn(aResponse().proxiedFrom(azuriteUrl).withFixedDelay(2000))
+                    .willSetStateTo("Success"));
+
+            // Third and subsequent renewal requests: no delay, just proxy
+            wireMockServer.stubFor(put(urlPathMatching(".*/oak/repo\\.lock"))
+                    .withQueryParam("comp", equalTo("lease"))
+                    .inScenario(scenarioName)
+                    .whenScenarioStateIs("Success")
+                    .willReturn(aResponse().proxiedFrom(azuriteUrl)));
+
+            // Create a CloudBlockBlob pointing to WireMock instead of Azurite
+            String wireMockEndpoint = "http://127.0.0.1:" + wireMockPort + "/devstoreaccount1";
+            String connectionString = "DefaultEndpointsProtocol=http;AccountName=" + AzuriteDockerRule.ACCOUNT_NAME
+                    + ";AccountKey=" + AzuriteDockerRule.ACCOUNT_KEY
+                    + ";BlobEndpoint=" + wireMockEndpoint;
+
+            CloudStorageAccount storageAccount = CloudStorageAccount.parse(connectionString);
+            CloudBlobClient blobClient = storageAccount.createCloudBlobClient();
+            CloudBlobContainer proxyContainer = blobClient.getContainerReference(container.getName());
+
+            CloudBlockBlob blob = proxyContainer.getBlockBlobReference("oak/repo.lock");
+
+            AtomicBoolean shutdownCalled = new AtomicBoolean(false);
+            Runnable shutdownHook = () -> shutdownCalled.set(true);
+
+            WriteAccessController writeAccessController = new WriteAccessController();
+            AzureRepositoryLockV8 lock = new AzureRepositoryLockV8(blob, shutdownHook, writeAccessController);
+            lock.lock();
+
+            // Wait for at least 3 lease renewal requests (2 timeouts + 1 success)
+            await().atMost(10, java.util.concurrent.TimeUnit.SECONDS)
+                    .untilAsserted(() -> wireMockServer.verify(
+                            moreThanOrExactly(3),
+                            putRequestedFor(urlPathMatching(".*/oak/repo\\.lock"))
+                                    .withQueryParam("comp", equalTo("lease"))));
+
+            assertFalse("Shutdown hook should not be called for client-side timeout exceptions", shutdownCalled.get());
+
+            lock.unlock();
+        } finally {
+            wireMockServer.stop();
+        }
+    }
 }