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); + } + +}
