001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.camel.util.backoff; 018 019import java.util.ArrayList; 020import java.util.List; 021import java.util.concurrent.ScheduledExecutorService; 022import java.util.concurrent.ScheduledFuture; 023import java.util.concurrent.TimeUnit; 024import java.util.concurrent.atomic.AtomicReference; 025import java.util.concurrent.locks.Lock; 026import java.util.concurrent.locks.ReentrantLock; 027import java.util.function.BiConsumer; 028 029import org.apache.camel.util.function.ThrowingFunction; 030 031public final class BackOffTimerTask implements BackOffTimer.Task, Runnable { 032 private final Lock lock = new ReentrantLock(); 033 private final BackOffTimer timer; 034 private final BackOff backOff; 035 private final ScheduledExecutorService scheduler; 036 private final ThrowingFunction<BackOffTimer.Task, Boolean, Exception> function; 037 private final AtomicReference<ScheduledFuture<?>> futureRef; 038 private final List<BiConsumer<BackOffTimer.Task, Throwable>> consumers; 039 040 private Status status; 041 private long firstAttemptTime; 042 private long currentAttempts; 043 private long currentDelay; 044 private long currentElapsedTime; 045 private long lastAttemptTime; 046 private long nextAttemptTime; 047 private Throwable cause; 048 049 public BackOffTimerTask(BackOffTimer timer, BackOff backOff, ScheduledExecutorService scheduler, 050 ThrowingFunction<BackOffTimer.Task, Boolean, Exception> function) { 051 this.timer = timer; 052 this.backOff = backOff; 053 this.scheduler = scheduler; 054 this.status = Status.Active; 055 056 this.currentAttempts = 0; 057 this.currentDelay = backOff.getDelay().toMillis(); 058 this.currentElapsedTime = 0; 059 this.firstAttemptTime = BackOff.NEVER; 060 this.lastAttemptTime = BackOff.NEVER; 061 this.nextAttemptTime = BackOff.NEVER; 062 063 this.function = function; 064 this.consumers = new ArrayList<>(); 065 this.futureRef = new AtomicReference<>(); 066 } 067 068 // ***************************** 069 // Properties 070 // ***************************** 071 072 @Override 073 public String getName() { 074 return timer.getName(); 075 } 076 077 @Override 078 public BackOff getBackOff() { 079 return backOff; 080 } 081 082 @Override 083 public Status getStatus() { 084 return status; 085 } 086 087 @Override 088 public long getCurrentAttempts() { 089 return currentAttempts; 090 } 091 092 @Override 093 public long getCurrentDelay() { 094 return currentDelay; 095 } 096 097 @Override 098 public long getCurrentElapsedTime() { 099 return currentElapsedTime; 100 } 101 102 @Override 103 public long getFirstAttemptTime() { 104 return firstAttemptTime; 105 } 106 107 @Override 108 public long getLastAttemptTime() { 109 return lastAttemptTime; 110 } 111 112 @Override 113 public long getNextAttemptTime() { 114 return nextAttemptTime; 115 } 116 117 @Override 118 public Throwable getException() { 119 return cause; 120 } 121 122 @Override 123 public void reset() { 124 this.currentAttempts = 0; 125 this.currentDelay = 0; 126 this.currentElapsedTime = 0; 127 this.firstAttemptTime = BackOff.NEVER; 128 this.lastAttemptTime = BackOff.NEVER; 129 this.nextAttemptTime = BackOff.NEVER; 130 this.status = Status.Active; 131 this.cause = null; 132 } 133 134 @Override 135 public void cancel() { 136 stop(); 137 138 ScheduledFuture<?> future = futureRef.get(); 139 if (future != null) { 140 future.cancel(true); 141 } 142 143 // signal task completion on cancel. 144 complete(null); 145 146 // the task is cancelled and should not be restarted so remove from timer 147 if (timer != null) { 148 timer.remove(this); 149 } 150 } 151 152 @Override 153 public void whenComplete(BiConsumer<BackOffTimer.Task, Throwable> whenCompleted) { 154 lock.lock(); 155 try { 156 if (backOff.isRemoveOnComplete()) { 157 timer.remove(this); 158 } 159 consumers.add(whenCompleted); 160 } finally { 161 lock.unlock(); 162 } 163 } 164 165 // ***************************** 166 // Task execution 167 // ***************************** 168 169 @Override 170 public void run() { 171 if (status == Status.Active) { 172 try { 173 lastAttemptTime = System.currentTimeMillis(); 174 if (firstAttemptTime < 0) { 175 firstAttemptTime = lastAttemptTime; 176 } 177 178 if (function.apply(this)) { 179 long delay = next(); 180 if (status != Status.Active) { 181 // if the call to next makes the context not more 182 // active, signal task completion. 183 complete(null); 184 } else { 185 nextAttemptTime = lastAttemptTime + delay; 186 187 // Cache the scheduled future so it can be cancelled 188 // later by Task.cancel() 189 futureRef.lazySet(scheduler.schedule(this, delay, TimeUnit.MILLISECONDS)); 190 } 191 } else { 192 stop(); 193 194 status = Status.Completed; 195 // if the function return false no more attempts should 196 // be made so stop the context. 197 complete(null); 198 } 199 } catch (Exception e) { 200 stop(); 201 202 status = Status.Failed; 203 complete(e); 204 } 205 } 206 } 207 208 void stop() { 209 this.currentAttempts = 0; 210 this.currentDelay = BackOff.NEVER; 211 this.currentElapsedTime = 0; 212 this.firstAttemptTime = BackOff.NEVER; 213 this.lastAttemptTime = BackOff.NEVER; 214 this.nextAttemptTime = BackOff.NEVER; 215 this.status = Status.Inactive; 216 } 217 218 void complete(Throwable throwable) { 219 this.cause = throwable; 220 lock.lock(); 221 try { 222 consumers.forEach(c -> c.accept(this, throwable)); 223 } finally { 224 lock.unlock(); 225 } 226 } 227 228 // ***************************** 229 // Impl 230 // ***************************** 231 232 /** 233 * Return the number of milliseconds to wait before retrying the operation or ${@link BackOff#NEVER} to indicate 234 * that no further attempt should be made. 235 */ 236 public long next() { 237 // A call to next when currentDelay is set to NEVER has no effects 238 // as this means that either the timer is exhausted or it has explicit 239 // stopped 240 if (status == Status.Active) { 241 242 currentAttempts++; 243 244 if (currentAttempts > backOff.getMaxAttempts()) { 245 currentDelay = BackOff.NEVER; 246 status = Status.Exhausted; 247 } else if (currentElapsedTime > backOff.getMaxElapsedTime().toMillis()) { 248 currentDelay = BackOff.NEVER; 249 status = Status.Exhausted; 250 } else { 251 if (currentDelay <= backOff.getMaxDelay().toMillis()) { 252 currentDelay = (long) (currentDelay * backOff.getMultiplier()); 253 } 254 255 currentElapsedTime += currentDelay; 256 } 257 } 258 259 return currentDelay; 260 } 261 262 @Override 263 public String toString() { 264 return "BackOffTimerTask[" 265 + "name=" + timer.getName() 266 + ", status=" + status 267 + ", currentAttempts=" + currentAttempts 268 + ", currentDelay=" + currentDelay 269 + ", currentElapsedTime=" + currentElapsedTime 270 + ", firstAttemptTime=" + firstAttemptTime 271 + ", lastAttemptTime=" + lastAttemptTime 272 + ", nextAttemptTime=" + nextAttemptTime 273 + ']'; 274 } 275}