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

joerghoh pushed a commit to branch feature/SLING-10418-retry
in repository 
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-jcr-repoinit.git

commit 8d102ce65cfb227d892ebde42a7b0b13333ab6bc
Author: Joerg Hoh <[email protected]>
AuthorDate: Mon Jun 7 16:14:01 2021 +0200

    SLING-10418 implement a retry for the application of the statements
---
 .../impl/RepositoryInitializerFactory.java         |  54 ++++++++--
 .../jcr/repoinit/impl/RetryableOperation.java      | 120 +++++++++++++++++++++
 .../jcr/repoinit/impl/RetryableOperationTest.java  |  87 +++++++++++++++
 3 files changed, 255 insertions(+), 6 deletions(-)

diff --git 
a/src/main/java/org/apache/sling/jcr/repoinit/impl/RepositoryInitializerFactory.java
 
b/src/main/java/org/apache/sling/jcr/repoinit/impl/RepositoryInitializerFactory.java
index 3eae372..9a52288 100644
--- 
a/src/main/java/org/apache/sling/jcr/repoinit/impl/RepositoryInitializerFactory.java
+++ 
b/src/main/java/org/apache/sling/jcr/repoinit/impl/RepositoryInitializerFactory.java
@@ -20,7 +20,16 @@ import java.io.StringReader;
 import java.util.Arrays;
 import java.util.List;
 
+import javax.jcr.AccessDeniedException;
+import javax.jcr.InvalidItemStateException;
+import javax.jcr.ItemExistsException;
+import javax.jcr.ReferentialIntegrityException;
+import javax.jcr.RepositoryException;
 import javax.jcr.Session;
+import javax.jcr.lock.LockException;
+import javax.jcr.nodetype.ConstraintViolationException;
+import javax.jcr.nodetype.NoSuchNodeTypeException;
+import javax.jcr.version.VersionException;
 
 import org.apache.sling.jcr.api.SlingRepository;
 import org.apache.sling.jcr.api.SlingRepositoryInitializer;
@@ -111,9 +120,9 @@ public class RepositoryInitializerFactory implements 
SlingRepositoryInitializer
                         }
                         final String repoinitText = p.getRepoinitText("raw:" + 
reference);
                         final List<Operation> ops = parser.parse(new 
StringReader(repoinitText));
-                        log.info("Executing {} repoinit operations", 
ops.size());
-                        processor.apply(s, ops);
-                        s.save();
+                        String msg = String.format("Executing %s repoinit 
operations", ops.size());
+                        log.info(msg);
+                        applyOperations(s,ops,msg);
                     }
                 }
                 if ( config.scripts() != null ) {
@@ -122,9 +131,9 @@ public class RepositoryInitializerFactory implements 
SlingRepositoryInitializer
                             continue;
                         }
                         final List<Operation> ops = parser.parse(new 
StringReader(script));
-                        log.info("Executing {} repoinit operations", 
ops.size());
-                        processor.apply(s, ops);
-                        s.save();
+                        String msg = String.format("Executing %s repoinit 
operations", ops.size());
+                        log.info(msg);
+                        applyOperations(s,ops,msg);
                     }
                 }
             } finally {
@@ -132,4 +141,37 @@ public class RepositoryInitializerFactory implements 
SlingRepositoryInitializer
             }
         }
     }
+
+
+    /**
+     * Apply the operations within a session, support retries
+     * @param session the JCR session to use
+     * @param ops the list of operations
+     * @param logMessage the messages to print when retry
+     * @throws Exception if the application fails despite the retry
+     */
+    private void applyOperations(Session session, List<Operation> ops, String 
logMessage) throws Exception {
+
+        RetryableOperation retry = new 
RetryableOperation.Builder().withBackoffBase(1000).withMaxRetries(3).build();
+        boolean successful = retry.apply(() -> {
+            try {
+                processor.apply(session, ops);
+                session.save();
+                return true;
+            } catch (RepositoryException e) {
+                log.error("(temporarily) failed to apply repoinit 
operations",e);
+                try {
+                    session.refresh(false); // discard all pending changes
+                } catch (RepositoryException e1) {
+                    // ignore
+                }
+                return false;
+            }
+        }, logMessage);
+        if (!successful) {
+            throw new RepositoryException("Eventually failed to apply repoinit 
statements, please check previous log messages");
+        }
+    }
+
+
 }
diff --git 
a/src/main/java/org/apache/sling/jcr/repoinit/impl/RetryableOperation.java 
b/src/main/java/org/apache/sling/jcr/repoinit/impl/RetryableOperation.java
new file mode 100644
index 0000000..572ea20
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/repoinit/impl/RetryableOperation.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.jcr.repoinit.impl;
+
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A simple implementation of retryable operations.
+ * Use the builder class to create an instance of it.
+ *
+ */
+public class RetryableOperation {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(RetryableOperation.class);
+
+    int backoffBase;
+    int maxRetries;
+    int jitter;
+
+    int retryCount = 0;
+
+    RetryableOperation(int backoff, int maxRetries, int jitter) {
+        this.backoffBase = backoff;
+        this.maxRetries = maxRetries;
+        this.jitter = jitter;
+    }
+    /**
+     * Execute the operation with the defined retry until it returns true or
+     * the retry aborts; in case the operation is retried, a log message is 
logged on INFO with the
+     * provided logMessage and the current number of retries
+     * @param operation
+     * @param logMessage the log message
+     * @return true if the supplier was eventually successful, false if it 
failed despite all retries
+     */
+    public boolean apply(Supplier<Boolean> operation, String logMessage) {
+
+        boolean successful = false;
+        successful = operation.get();
+        while (! successful && retryCount < maxRetries) {
+            retryCount++;
+            LOG.info("%s (retry %d/%d)", logMessage, retryCount, maxRetries);
+            delay(retryCount);
+            successful = operation.get();
+        }
+        return successful;
+    }
+
+    private void delay(int retryCount) {
+
+        int j = (int) (Math.random() * (jitter));
+        int delayInMilis = (backoffBase * retryCount) + j;
+        try {
+            TimeUnit.MILLISECONDS.sleep(delayInMilis);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+
+
+
+    public static class Builder {
+
+        int exponentialBackoff = 1000; // default
+        int maxRetries = 3; // default
+        int jitter = 200;
+
+        /**
+         * The backoff time
+         * @param msec backoff time in miliseconds
+         * @return the builder
+         */
+        Builder withBackoffBase(int msec) {
+            exponentialBackoff = msec;
+            return this;
+        }
+
+        /**
+         * Configures the number of retries;
+         * @param retries number of retries
+         * @return the builder
+         */
+        Builder withMaxRetries(int retries) {
+            this.maxRetries= retries;
+            return this;
+        }
+
+        /**
+         * configures the jitter
+         * @param msec the jitter in miliseconds
+         * @return the builder
+         */
+        Builder withJitter(int msec) {
+            this.jitter = msec;
+            return this;
+        }
+
+        RetryableOperation build() {
+            return new RetryableOperation(exponentialBackoff,maxRetries, 
jitter);
+        }
+    }
+
+}
diff --git 
a/src/test/java/org/apache/sling/jcr/repoinit/impl/RetryableOperationTest.java 
b/src/test/java/org/apache/sling/jcr/repoinit/impl/RetryableOperationTest.java
new file mode 100644
index 0000000..435d5c8
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/jcr/repoinit/impl/RetryableOperationTest.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.jcr.repoinit.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+
+import org.junit.Test;
+
+
+public class RetryableOperationTest {
+
+
+    @Test
+    public void testWithoutRetry() {
+
+        RetryableOperation ro = new RetryableOperation.Builder().build();
+        Supplier<Boolean> op = () -> {
+            return true;
+        };
+        boolean successful = ro.apply(op, "log");
+        assertEquals(ro.retryCount,0);
+        assertTrue(successful);
+    }
+
+    @Test
+    public void testWithRetrySuccesful() {
+
+        // bypass the effective final feature
+        AtomicInteger retries = new AtomicInteger(0);
+        RetryableOperation ro = new RetryableOperation.Builder()
+                .withBackoffBase(10)
+                .withMaxRetries(3)
+                .build();
+        Supplier<Boolean> op = () -> {
+            // 1 regular execution + 2 retries
+            if (retries.getAndAdd(1) == 2) {
+                return true;
+            } else {
+                return false;
+            }
+        };
+        boolean successful = ro.apply(op, "log");
+        assertEquals(2,ro.retryCount);
+        assertTrue(successful);
+    }
+
+    @Test
+    public void testWithRetryFail() {
+
+        AtomicInteger retries = new AtomicInteger(0);
+        RetryableOperation ro = new RetryableOperation.Builder()
+                .withBackoffBase(10)
+                .withMaxRetries(3)
+                .build();
+        Supplier<Boolean> op = () -> {
+            // 1 regular execution + 4 retries
+            if (retries.getAndAdd(1) == 4) {
+                return true;
+            } else {
+                return false;
+            }
+        };
+        boolean successful = ro.apply(op, "log");
+        assertEquals(3,ro.retryCount); //only 3 retries and then stopped
+        assertFalse(successful);
+    }
+
+}

Reply via email to