From db40facb2d78d3ffbd569a7afdbc9783625f2c2e Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Mon, 14 May 2012 23:02:07 -0700 Subject: [PATCH] Issue 930:RetryingCacheLoaderDecorator --- .../jclouds/cache/ForwardingCacheLoader.java | 79 +++++++ .../cache/RetryingCacheLoaderDecorator.java | 196 ++++++++++++++++++ ...ntiallyAndRetryOnThrowableCacheLoader.java | 128 ++++++++++++ ...onentiallyAndRetryOnThrowableCallable.java | 99 +++++++++ .../cache/ForwardingCacheLoaderTest.java | 82 ++++++++ .../RetryingCacheLoaderDecoratorTest.java | 85 ++++++++ ...llyAndRetryOnThrowableCacheLoaderTest.java | 76 +++++++ ...tiallyAndRetryOnThrowableCallableTest.java | 75 +++++++ 8 files changed, 820 insertions(+) create mode 100644 core/src/main/java/org/jclouds/cache/ForwardingCacheLoader.java create mode 100644 core/src/main/java/org/jclouds/cache/RetryingCacheLoaderDecorator.java create mode 100644 core/src/main/java/org/jclouds/cache/internal/BackoffExponentiallyAndRetryOnThrowableCacheLoader.java create mode 100644 core/src/main/java/org/jclouds/cache/internal/BackoffExponentiallyAndRetryOnThrowableCallable.java create mode 100644 core/src/test/java/org/jclouds/cache/ForwardingCacheLoaderTest.java create mode 100644 core/src/test/java/org/jclouds/cache/RetryingCacheLoaderDecoratorTest.java create mode 100644 core/src/test/java/org/jclouds/cache/internal/BackoffExponentiallyAndRetryOnThrowableCacheLoaderTest.java create mode 100644 core/src/test/java/org/jclouds/cache/internal/BackoffExponentiallyAndRetryOnThrowableCallableTest.java diff --git a/core/src/main/java/org/jclouds/cache/ForwardingCacheLoader.java b/core/src/main/java/org/jclouds/cache/ForwardingCacheLoader.java new file mode 100644 index 0000000000..18c203274f --- /dev/null +++ b/core/src/main/java/org/jclouds/cache/ForwardingCacheLoader.java @@ -0,0 +1,79 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.jclouds.cache; + +import java.util.Map; + +import com.google.common.annotations.Beta; +import com.google.common.base.Preconditions; +import com.google.common.cache.CacheLoader; +import com.google.common.util.concurrent.ListenableFuture; + +/** + * A {@link CacheLoader} which forwards all its method calls to another {@link CacheLoader}. + * Subclasses should override one or more methods to modify the behavior of the backing cache as + * desired per the decorator pattern. + * + * @author Adrian Cole + * @since 1.5 + */ +@Beta +public abstract class ForwardingCacheLoader extends CacheLoader { + + /** Constructor for use by subclasses. */ + protected ForwardingCacheLoader() { + } + + protected abstract CacheLoader delegate(); + + @Override + public V load(K key) throws Exception { + return delegate().load(key); + } + + @Override + public ListenableFuture reload(K key, V oldValue) throws Exception { + return delegate().reload(key, oldValue); + } + + @Override + public Map loadAll(Iterable keys) throws Exception { + return delegate().loadAll(keys); + } + + /** + * A simplified version of {@link ForwardingCacheLoader} where subclasses can pass in an already + * constructed {@link CacheLoader} as the delegete. + * + */ + @Beta + public static class SimpleForwardingCacheLoader extends ForwardingCacheLoader { + private final CacheLoader delegate; + + protected SimpleForwardingCacheLoader(CacheLoader delegate) { + this.delegate = Preconditions.checkNotNull(delegate); + } + + @Override + protected final CacheLoader delegate() { + return delegate; + } + + } +} \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/cache/RetryingCacheLoaderDecorator.java b/core/src/main/java/org/jclouds/cache/RetryingCacheLoaderDecorator.java new file mode 100644 index 0000000000..46a925c242 --- /dev/null +++ b/core/src/main/java/org/jclouds/cache/RetryingCacheLoaderDecorator.java @@ -0,0 +1,196 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.jclouds.cache; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import org.jclouds.cache.internal.BackoffExponentiallyAndRetryOnThrowableCacheLoader; + +import com.google.common.annotations.Beta; +import com.google.common.base.Objects; +import com.google.common.cache.CacheLoader; + +/** + *

+ * A decorator of {@link CacheLoader} instances having any combination of the following features: + * + *

    + *
  • exponential backoff based on a particular Throwable type + *
+ * + * These features are all optional; cache loaders can be created using all or none of them. By + * default, the input cache loader is returned unaffected. + * + *

+ * Usage example: + * + *

+ * @code
+ * 
+ *   CacheLoader loader = RetryingCacheLoaderDecorator.newDecorator()
+ *       .on(ResourceNotFoundException.class).exponentiallyBackoff()
+ *       .decorate(
+ *           new CacheLoader() {
+ *             public Graph load(Key key) throws AnyException {
+ *               return createOnFlakeyConnection(key);
+ *             }
+ *           });}
+ * 
+ * + * @param + * the base key type for all cache loaders created by this decorator + * @param + * the base value type for all cache loaders created by this decorator + * @author Adrian Cole + * @since 1.5 + */ +@Beta +public class RetryingCacheLoaderDecorator { + protected RetryingCacheLoaderDecorator() { + } + + /** + * Constructs a new {@code RetryingCacheLoaderDecorator} instance with default settings, and no + * retrying any kind. + */ + // we can resolve generic type during decorate, as opposed to here + public static RetryingCacheLoaderDecorator newDecorator() { + return new RetryingCacheLoaderDecorator(); + } + + /** + * Determines the action to carry out on a particular throwable. + * + */ + public OnThrowableBuilder on(Class retryableThrowable) { + return new OnThrowableBuilder(retryableThrowable); + } + + public static class OnThrowableBuilder { + Class retryableThrowable; + + protected OnThrowableBuilder(Class retryableThrowable) { + this.retryableThrowable = checkNotNull(retryableThrowable, "retryableThrowable"); + } + + /** + * For each attempt, exponentially backoff + */ + public BackoffExponentiallyAndRetryOnThrowableCacheLoaderDecorator exponentiallyBackoff() { + return new BackoffExponentiallyAndRetryOnThrowableCacheLoaderDecorator(retryableThrowable); + } + + } + + public static class BackoffExponentiallyAndRetryOnThrowableCacheLoaderDecorator extends + RetryingCacheLoaderDecorator { + private long periodMs = 100l; + private long maxPeriodMs = 200l; + private int maxTries = 5; + private final Class retryableThrowable; + + protected BackoffExponentiallyAndRetryOnThrowableCacheLoaderDecorator(Class retryableThrowable) { + this.retryableThrowable = checkNotNull(retryableThrowable, "retryableThrowable"); + } + + /** + * The initial period in milliseconds to delay between tries. with each try this period will + * increase exponentially. + *

+ * default: {@code 100} + * + */ + public BackoffExponentiallyAndRetryOnThrowableCacheLoaderDecorator periodMs(long periodMs) { + checkArgument(periodMs > 1, "maxTries must be positive: %d", periodMs); + this.periodMs = periodMs; + return this; + } + + /** + * The initial period in milliseconds to delay between tries. with each try this period will + * increase exponentially. + *

+ * default: {@code 200} + * + */ + public BackoffExponentiallyAndRetryOnThrowableCacheLoaderDecorator maxPeriodMs(long maxPeriodMs) { + checkArgument(maxPeriodMs > periodMs, "maxPeriodMs must be equal to or greater than periodMs: %d %d", + maxPeriodMs, periodMs); + this.maxPeriodMs = maxPeriodMs; + return this; + } + + /** + * The maximum attempts to try on the given exception type + *

+ * default: {@code 5} + * + */ + public BackoffExponentiallyAndRetryOnThrowableCacheLoaderDecorator maxTries(int maxTries) { + checkArgument(maxTries > 1, "maxTries must be more than one: %d", maxTries); + this.maxTries = maxTries; + return this; + } + + @Override + public CacheLoader decorate(CacheLoader loader) { + return new BackoffExponentiallyAndRetryOnThrowableCacheLoader(retryableThrowable, periodMs, + maxPeriodMs, maxTries, super.decorate(loader)); + } + + @Override + protected Objects.ToStringHelper string() { + return string().add("retryableThrowable", retryableThrowable).add("periodMs", periodMs).add("maxPeriodMs", + maxPeriodMs).add("maxTries", maxTries); + } + } + + /** + * Decorates a cacheloader, or returns the same value, if no retrying features were requested. + * + *

+ * This method does not alter the state of this {@code RetryingCacheLoaderDecorator} instance, so + * it can be invoked again to create multiple independent cache loaders. + * + * @param loader + * the cache loader used to obtain new values + * @return a cache loader having the requested features + */ + public CacheLoader decorate(CacheLoader loader) { + return (CacheLoader) loader; + } + + /** + * Returns a string representation for this RetryingCacheLoaderDecorator instance. The exact form + * of the returned string is not specified. + */ + @Override + public String toString() { + return string().toString(); + } + + /** + * append any state that should be considered in {@link #toString} here. + */ + protected Objects.ToStringHelper string() { + return Objects.toStringHelper(this); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/cache/internal/BackoffExponentiallyAndRetryOnThrowableCacheLoader.java b/core/src/main/java/org/jclouds/cache/internal/BackoffExponentiallyAndRetryOnThrowableCacheLoader.java new file mode 100644 index 0000000000..967a046143 --- /dev/null +++ b/core/src/main/java/org/jclouds/cache/internal/BackoffExponentiallyAndRetryOnThrowableCacheLoader.java @@ -0,0 +1,128 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.jclouds.cache.internal; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Map; +import java.util.concurrent.Callable; + +import org.jclouds.cache.ForwardingCacheLoader; + +import com.google.common.annotations.Beta; +import com.google.common.cache.CacheLoader; +import com.google.common.util.concurrent.ListenableFuture; + +/** + * Exponentially backs off, if we encounter an exception of the given type during any of the + * following methods: + *

    + *
  • load + *
  • reload + *
  • loadAll + *
+ * + * @param + * the key type of the cache loader + * @param + * the value type of the cache loader + * @author Adrian Cole + * @since 1.5 + */ +@Beta +public class BackoffExponentiallyAndRetryOnThrowableCacheLoader extends ForwardingCacheLoader { + private final Class retryableThrowable; + private final long periodMs; + private final long maxPeriodMs; + private final int maxTries; + private final CacheLoader loader; + + /** + * + * @param retryableThrowable + * the exception which we can retry + * @param periodMs + * initial period, which exponentially increases with each try, specified in + * milliseconds + * @param maxPeriodMs + * maximum period duration, specified in milliseconds + * @param maxTries + * maximum amount of tries + * @param loader + * the loader we are able to retry + */ + public BackoffExponentiallyAndRetryOnThrowableCacheLoader(Class retryableThrowable, long periodMs, + long maxPeriodMs, int maxTries, CacheLoader loader) { + this.retryableThrowable = checkNotNull(retryableThrowable, "retryableThrowable"); + checkArgument(maxTries > 1, "maxTries must be more than one: %d", maxTries); + this.maxTries = maxTries; + checkArgument(periodMs > 0, "periodMs must be positive: %d", periodMs); + this.periodMs = periodMs; + checkArgument(maxPeriodMs > periodMs, "maxPeriodMs must be equal to or greater than periodMs: %d %d", + maxPeriodMs, periodMs); + this.maxPeriodMs = maxPeriodMs; + this.loader = checkNotNull(loader, "loader"); + } + + @Override + protected CacheLoader delegate() { + return loader; + } + + // TODO: refactor into a better closure in java pattern, if one exists + @Override + public V load(final K key) throws Exception { + return backoffExponentiallyAndRetryOnThrowable(new Callable() { + + @Override + public V call() throws Exception { + return BackoffExponentiallyAndRetryOnThrowableCacheLoader.super.load(key); + } + }); + } + + @Override + public ListenableFuture reload(final K key, final V oldValue) throws Exception { + return backoffExponentiallyAndRetryOnThrowable(new Callable>() { + + @Override + public ListenableFuture call() throws Exception { + return BackoffExponentiallyAndRetryOnThrowableCacheLoader.super.reload(key, oldValue); + } + }); + } + + @Override + public Map loadAll(final Iterable keys) throws Exception { + return backoffExponentiallyAndRetryOnThrowable(new Callable>() { + + @Override + public Map call() throws Exception { + return BackoffExponentiallyAndRetryOnThrowableCacheLoader.super.loadAll(keys); + } + }); + } + + private T backoffExponentiallyAndRetryOnThrowable(Callable callable) throws Exception { + return new BackoffExponentiallyAndRetryOnThrowableCallable(retryableThrowable, periodMs, maxPeriodMs, + maxTries, callable).call(); + } + +} \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/cache/internal/BackoffExponentiallyAndRetryOnThrowableCallable.java b/core/src/main/java/org/jclouds/cache/internal/BackoffExponentiallyAndRetryOnThrowableCallable.java new file mode 100644 index 0000000000..07709edf57 --- /dev/null +++ b/core/src/main/java/org/jclouds/cache/internal/BackoffExponentiallyAndRetryOnThrowableCallable.java @@ -0,0 +1,99 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.jclouds.cache.internal; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.concurrent.Callable; + +import org.jclouds.util.Throwables2; + +import com.google.common.annotations.Beta; +import com.google.common.base.Objects; +import com.google.common.base.Throwables; +import com.google.common.collect.ForwardingObject; + +/** + * Exponentially backs off, if we encounter an exception of the given type during + * {@link Callable#call} + * + * @author Adrian Cole + * @since 1.5 + */ +@Beta +class BackoffExponentiallyAndRetryOnThrowableCallable extends ForwardingObject implements Callable { + private final Class retryableThrowable; + private final long periodMs; + private final long maxPeriodMs; + private final int maxTries; + private final Callable callable; + + BackoffExponentiallyAndRetryOnThrowableCallable(Class retryableThrowable, long periodMs, + long maxPeriodMs, int maxTries, Callable callable) { + this.retryableThrowable = checkNotNull(retryableThrowable, "retryableThrowable"); + checkArgument(maxTries > 1, "maxTries must be more than one: %d", maxTries); + this.maxTries = maxTries; + checkArgument(periodMs > 0, "periodMs must be positive: %d", periodMs); + this.periodMs = periodMs; + checkArgument(maxPeriodMs > periodMs, "maxPeriodMs must be equal to or greater than periodMs: %d %d", + maxPeriodMs, periodMs); + this.maxPeriodMs = maxPeriodMs; + this.callable = checkNotNull(callable, "callable"); + } + + @Override + protected Callable delegate() { + return callable; + } + + @Override + public T call() throws Exception { + Exception currentException = null; + for (int currentTries = 0; currentTries < maxTries; currentTries++) { + try { + return delegate().call(); + } catch (Exception e) { + currentException = e; + if (Throwables2.getFirstThrowableOfType(e, retryableThrowable) != null) { + imposeBackoffExponentialDelay(periodMs, maxPeriodMs, 2, currentTries, maxTries); + } else { + throw e; + } + } + } + throw currentException; + } + + void imposeBackoffExponentialDelay(long period, long maxPeriod, int pow, int failureCount, int max) { + long delayMs = (long) (period * Math.pow(failureCount, pow)); + delayMs = delayMs > maxPeriod ? maxPeriod : delayMs; + try { + Thread.sleep(delayMs); + } catch (InterruptedException e) { + Throwables.propagate(e); + } + } + + @Override + public String toString() { + return Objects.toStringHelper("").add("retryableThrowable", retryableThrowable).add("periodMs", periodMs).add( + "maxPeriodMs", maxPeriodMs).add("maxTries", maxTries).add("callable", callable).toString(); + } +} \ No newline at end of file diff --git a/core/src/test/java/org/jclouds/cache/ForwardingCacheLoaderTest.java b/core/src/test/java/org/jclouds/cache/ForwardingCacheLoaderTest.java new file mode 100644 index 0000000000..bd07911b12 --- /dev/null +++ b/core/src/test/java/org/jclouds/cache/ForwardingCacheLoaderTest.java @@ -0,0 +1,82 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.jclouds.cache; + +import static org.easymock.EasyMock.createMockBuilder; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertSame; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.google.common.cache.CacheLoader; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +/** + * Unit test for {@link ForwardingCacheLoader}. + * + * @author Adrian Cole + */ +@Test(testName = "ForwardingCacheLoaderTest", singleThreaded = true) +public class ForwardingCacheLoaderTest { + private CacheLoader forward; + private CacheLoader mock; + + @SuppressWarnings("unchecked") + @BeforeMethod + public void setUp() { + // add mocked methods for default forwarded ones + mock = createMockBuilder(CacheLoader.class).addMockedMethods("loadAll", "reload").createMock(); + forward = new ForwardingCacheLoader() { + @Override + protected CacheLoader delegate() { + return mock; + } + }; + } + + public void testLoad() throws Exception { + expect(mock.load("key")).andReturn(Boolean.TRUE); + replay(mock); + assertSame(Boolean.TRUE, forward.load("key")); + verify(mock); + } + + public void testReload() throws Exception { + ListenableFuture trueF = Futures.immediateFuture(true); + expect(mock.reload("key", false)).andReturn(trueF); + replay(mock); + assertSame(forward.reload("key", false), trueF); + verify(mock); + } + + public void testLoadAll() throws Exception { + expect(mock.loadAll(ImmutableList.of("key"))).andReturn(ImmutableMap.of("key", Boolean.TRUE)); + replay(mock); + assertEquals(ImmutableMap.of("key", Boolean.TRUE), forward.loadAll(ImmutableList.of("key"))); + verify(mock); + } +} \ No newline at end of file diff --git a/core/src/test/java/org/jclouds/cache/RetryingCacheLoaderDecoratorTest.java b/core/src/test/java/org/jclouds/cache/RetryingCacheLoaderDecoratorTest.java new file mode 100644 index 0000000000..38c52aa7a8 --- /dev/null +++ b/core/src/test/java/org/jclouds/cache/RetryingCacheLoaderDecoratorTest.java @@ -0,0 +1,85 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.jclouds.cache; + +import static org.easymock.EasyMock.createMockBuilder; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; + +import org.jclouds.rest.ResourceNotFoundException; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.google.common.cache.CacheLoader; + +/** + * Unit tests for RetryingCacheLoaderDecorator. + * + * @author Adrian Cole + */ +@Test(testName = "ForwardingCacheLoaderTest", singleThreaded = true) +public class RetryingCacheLoaderDecoratorTest { + private CacheLoader mock; + + @SuppressWarnings("unchecked") + @BeforeMethod + public void setUp() { + // add mocked methods for default forwarded ones + mock = createMockBuilder(CacheLoader.class).addMockedMethods("loadAll", "reload").createMock(); + } + + public void testNewDecoratorDecorateSameWhenNoParams() throws Exception { + assertSame(mock, RetryingCacheLoaderDecorator.newDecorator().decorate(mock)); + } + + @Test + void testDefaultMaxTriesIs5() throws Exception { + CacheLoader backoff = RetryingCacheLoaderDecorator.newDecorator().on( + ResourceNotFoundException.class).exponentiallyBackoff().decorate(mock); + + expect(mock.load("foo")).andThrow(new ResourceNotFoundException()).times(4); + expect(mock.load("foo")).andReturn(Boolean.TRUE); + + replay(mock); + assertSame(backoff.load("foo"), Boolean.TRUE); + verify(mock); + } + + @Test + void testMaxRetriesExceededThrowsException() throws Exception { + CacheLoader backoff = RetryingCacheLoaderDecorator.newDecorator() + .on(ResourceNotFoundException.class).exponentiallyBackoff() + .decorate(mock); + + expect(mock.load("foo")).andThrow(new ResourceNotFoundException()).times(5); + + replay(mock); + try { + backoff.load("foo"); + assertTrue(false); + } catch (ResourceNotFoundException e) { + + } + verify(mock); + } +} \ No newline at end of file diff --git a/core/src/test/java/org/jclouds/cache/internal/BackoffExponentiallyAndRetryOnThrowableCacheLoaderTest.java b/core/src/test/java/org/jclouds/cache/internal/BackoffExponentiallyAndRetryOnThrowableCacheLoaderTest.java new file mode 100644 index 0000000000..f0ce93213d --- /dev/null +++ b/core/src/test/java/org/jclouds/cache/internal/BackoffExponentiallyAndRetryOnThrowableCacheLoaderTest.java @@ -0,0 +1,76 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.jclouds.cache.internal; + +import static org.easymock.EasyMock.createMockBuilder; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; + +import org.jclouds.rest.ResourceNotFoundException; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.google.common.cache.CacheLoader; + +@Test(groups = "unit", testName = "BackoffExponentiallyAndRetryOnThrowableCacheLoaderTest") +public class BackoffExponentiallyAndRetryOnThrowableCacheLoaderTest { + private CacheLoader mock; + + @SuppressWarnings("unchecked") + @BeforeMethod + public void setUp() { + // add mocked methods for default forwarded ones + mock = createMockBuilder(CacheLoader.class).addMockedMethods("loadAll", "reload").createMock(); + } + + @Test + void testMaxRetriesNotExceededReturnsValue() throws Exception { + int attempts = 3; + BackoffExponentiallyAndRetryOnThrowableCacheLoader backoff = new BackoffExponentiallyAndRetryOnThrowableCacheLoader( + ResourceNotFoundException.class, 50l, 500l, attempts, mock); + + expect(mock.load("foo")).andThrow(new ResourceNotFoundException()).times(attempts - 1); + expect(mock.load("foo")).andReturn(Boolean.TRUE); + + replay(mock); + assertSame(backoff.load("foo"), Boolean.TRUE); + verify(mock); + } + + @Test + void testMaxRetriesExceededThrowsException() throws Exception { + int attempts = 3; + BackoffExponentiallyAndRetryOnThrowableCacheLoader backoff = new BackoffExponentiallyAndRetryOnThrowableCacheLoader( + ResourceNotFoundException.class, 50l, 500l, attempts, mock); + + expect(mock.load("foo")).andThrow(new ResourceNotFoundException()).times(attempts); + + replay(mock); + try { + backoff.load("foo"); + assertTrue(false); + } catch (ResourceNotFoundException e) { + + } + verify(mock); + } +} \ No newline at end of file diff --git a/core/src/test/java/org/jclouds/cache/internal/BackoffExponentiallyAndRetryOnThrowableCallableTest.java b/core/src/test/java/org/jclouds/cache/internal/BackoffExponentiallyAndRetryOnThrowableCallableTest.java new file mode 100644 index 0000000000..484cea707f --- /dev/null +++ b/core/src/test/java/org/jclouds/cache/internal/BackoffExponentiallyAndRetryOnThrowableCallableTest.java @@ -0,0 +1,75 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.jclouds.cache.internal; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import java.util.concurrent.Callable; + +import org.jclouds.rest.ResourceNotFoundException; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +@Test(groups = "unit", testName = "BackoffExponentiallyAndRetryOnThrowableCallableTest") +public class BackoffExponentiallyAndRetryOnThrowableCallableTest { + private Callable mock; + + @SuppressWarnings("unchecked") + @BeforeMethod + public void setUp() { + mock = createMock(Callable.class); + } + + @Test + void testMaxRetriesNotExceededReturnsValue() throws Exception { + int attempts = 3; + BackoffExponentiallyAndRetryOnThrowableCallable backoff = new BackoffExponentiallyAndRetryOnThrowableCallable( + ResourceNotFoundException.class, 50l, 500l, attempts, mock); + + expect(mock.call()).andThrow(new ResourceNotFoundException()).times(attempts - 1); + expect(mock.call()).andReturn("foo"); + + replay(mock); + assertEquals(backoff.call(), "foo"); + verify(mock); + } + + @Test + void testMaxRetriesExceededThrowsException() throws Exception { + int attempts = 3; + BackoffExponentiallyAndRetryOnThrowableCallable backoff = new BackoffExponentiallyAndRetryOnThrowableCallable( + ResourceNotFoundException.class, 50l, 500l, attempts, mock); + + expect(mock.call()).andThrow(new ResourceNotFoundException()).times(attempts); + + replay(mock); + try { + backoff.call(); + assertTrue(false); + } catch (ResourceNotFoundException e) { + + } + verify(mock); + } +} \ No newline at end of file