/*
 * Copyright Myrrix Ltd
 *
 * Licensed 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 net.myrrix.common;

import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.google.common.base.Preconditions;

/**
 * Encapsulates a reference to something that is created the first time it is needed. Instead
 * of providing an initial value, a {@link Callable} is given that can create the thing is provided.
 * This can also periodically re-create or reload the value.
 *
 * @author Sean Owen
 */
public final class ReloadingReference<V> {

  public static final long NO_RELOAD = -1L;

  private V value;
  private final Callable<V> retriever;
  private long lastRetrieval;
  private final long durationMS;
  private final Lock lock;

  public ReloadingReference(Callable<V> retriever) {
    this(retriever, NO_RELOAD, null);
  }

  /**
   * @param retriever object that can supply a new, current value of the reference
   * @param duration time after which a new value will be loaded, or {@link #NO_RELOAD} to not reload
   *  after the initial value is loaded
   * @param timeUnit units of time for duration
   */
  public ReloadingReference(Callable<V> retriever, long duration, TimeUnit timeUnit) {
    Preconditions.checkNotNull(retriever);
    this.retriever = retriever;
    if (duration == NO_RELOAD) {
      durationMS = NO_RELOAD;
    } else {
      Preconditions.checkArgument(duration > 0, "Duration must be positive: %s", duration);
      Preconditions.checkNotNull(timeUnit);
      durationMS = TimeUnit.MILLISECONDS.convert(duration, timeUnit);
    }
    lock = new ReentrantLock();
  }

  /**
   * Like {@link #get()}, but when a value already exists, will only wait for the given amount of time before
   * just proceeding to return the existing value instead of waiting for a new value to load.
   */
  public V get(long timeout, TimeUnit timeUnit) {
    try {
      if (lock.tryLock(timeout, timeUnit)) {
        try {
          doGet();
        } finally {
          lock.unlock();
        }
      }
      // timed out
    } catch (InterruptedException ie) {
      // interrupted
    }
    V theValue = value;
    return theValue == null ? get() : theValue;
  }

  /**
   * @return object that is returned by the provided {@link Callable}. If not yet created, it will block and
   *  wait for creation. If already created, it will return the existing value. The value will be re-created
   *  periodically, if the object has been configured to, and when this is needed, this method will again
   *  block while the value is re-created.
   */
  public V get() {
    lock.lock();
    try {
      doGet();
    } finally {
      lock.unlock();
    }
    return value;
  }

  private void doGet() {
    boolean reloading = durationMS > 0L;
    long now = reloading ? System.currentTimeMillis() : 0L;
    if (value == null || (durationMS > 0L && now > lastRetrieval + durationMS)) {
      try {
        value = retriever.call();
      } catch (RuntimeException re) {
        throw re;
      } catch (Exception e) {
        throw new IllegalStateException(e);
      }
      lastRetrieval = now;
    }
  }

  /**
   * @return object that is returned by the provided {@link Callable}, if it has already been created
   *  previously, or {@code null} otherwise
   */
  public V maybeGet() {
    return value;
  }

  /**
   * Clears the reference, requiring a load on next access.
   */
  public void clear() {
    value = null;
  }

}

@@ -822,16 +822,63 @@
 ditions;
+%0Aimport org.apache.commons.math3.util.FastMath;
 %0A%0A/**%0A *
@@ -1353,25 +1353,67 @@
  final long 
-d
+originalDurationMS;%0A  private long currentD
 urationMS;%0A 
@@ -2017,25 +2017,33 @@
 AD) %7B%0A      
-d
+originalD
 urationMS = 
@@ -2208,17 +2208,25 @@
 ;%0A      
-d
+originalD
 urationM
@@ -2282,24 +2282,68 @@
 nit);%0A    %7D%0A
+    currentDurationMS = originalDurationMS;%0A
     lock = n
@@ -3502,17 +3502,25 @@
 ading = 
-d
+originalD
 urationM
@@ -3617,23 +3617,17 @@
 %7C%7C (
-durationMS %3E 0L
+reloading
  && 
@@ -3648,17 +3648,24 @@
 ieval + 
-d
+currentD
 urationM
@@ -3735,15 +3735,8 @@
 ch (
-Runtime
 Exce
@@ -3741,17 +3741,16 @@
 ception 
-r
 e) %7B%0A   
@@ -3758,126 +3758,331 @@
     
-throw re;
+// Kind of arbitrary exponential backoff -- 2x after each error up to 16x
 %0A      
-%7D catch (Exception e) %7B%0A        throw new IllegalStateException(e);%0A      %7D%0A      lastRetrieval = now;
+  currentDurationMS = FastMath.min(currentDurationMS * 2, originalDurationMS * 16);%0A        throw new IllegalStateException(e);%0A      %7D%0A      lastRetrieval = now;      %0A      // Reset backoff%0A      currentDurationMS = originalDurationMS;      
 %0A   

Add exponential backoff to ReloadingReference