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}