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.wicket.util.thread;
018
019import java.time.Duration;
020import java.time.Instant;
021
022import org.slf4j.Logger;
023import org.slf4j.LoggerFactory;
024
025
026/**
027 * Runs a block of code periodically. A <code>Task</code> can be started at a given time in the
028 * future and can be a daemon. The block of code will be passed a <code>Log</code> object each time
029 * it is run through its <code>ICode</code> interface.
030 * <p>
031 * If the code block takes longer than the period to run, the next task invocation will occur
032 * immediately. In this case, tasks will not occur at precise multiples of the period. For example,
033 * if you run a task every 30 seconds, and the first run takes 40 seconds but the second takes 20
034 * seconds, your task will be invoked at 0 seconds, 40 seconds and 70 seconds (40 seconds + 30
035 * seconds), which is not an even multiple of 30 seconds.
036 * <p>
037 * In general, this is a simple task class designed for polling activities. If you need precise
038 * guarantees, you probably should be using a different task class.
039 * 
040 * @author Jonathan Locke
041 * @since 1.2.6
042 */
043public final class Task
044{
045        /** <code>true</code> if the task's thread should be a daemon */
046        private boolean isDaemon = true;
047
048        /** <code>true</code> if the task's thread has already started executing */
049        private boolean isStarted = false;
050
051        /** the <code>log</code> to give to the user's code */
052        private Logger log = null;
053
054        /** the name of this <code>Task</code> */
055        private final String name;
056
057        /** the <code>Instant</code> at which the task should start */
058        private Instant startTime = Instant.now();
059
060        /** When set the task will stop as soon as possible */
061        private boolean stop;
062
063        /** each <code>Task</code> has an associated <code>Thread</code> */
064        private Thread thread;
065
066        /**
067         * Constructor.
068         * 
069         * @param name
070         *            the name of this <code>Task</code>
071         */
072        public Task(final String name)
073        {
074                this.name = name;
075        }
076
077        /**
078         * Runs this <code>Task</code> at the given frequency. You may only call this method if the task
079         * has not yet been started. If the task is already running, an
080         * <code>IllegalStateException</code> will be thrown.
081         * 
082         * @param frequency
083         *            the frequency at which to run the code
084         * @param code
085         *            the code to run
086         * @throws IllegalStateException
087         *             thrown if task is already running
088         */
089        public synchronized final void run(final Duration frequency, final ICode code)
090        {
091                if (!isStarted)
092                {
093                        final Runnable runnable = new Runnable()
094                        {
095                                @Override
096                                public void run()
097                                {
098                                        // Sleep until start time
099                                        Duration untilStart = Duration.between(startTime, Instant.now());
100
101                                        final Logger log = getLog();
102
103                                        if (!untilStart.isNegative())
104                                        {
105                                                try
106                                                {
107                                                        Thread.sleep(untilStart.toMillis());
108                                                }
109                                                catch (InterruptedException e)
110                                                {
111                                                        log.error("An error occurred during sleeping phase.", e);
112                                                }
113                                        }
114
115                                        try
116                                        {
117                                                while (!stop)
118                                                {
119                                                        // Get the start of the current period
120                                                        final Instant startOfPeriod = Instant.now();
121
122                                                        if (log.isTraceEnabled())
123                                                        {
124                                                                log.trace("Run the job: '{}'", code);
125                                                        }
126
127                                                        try
128                                                        {
129                                                                // Run the user's code
130                                                                code.run(getLog());
131                                                        }
132                                                        catch (Exception e)
133                                                        {
134                                                                log.error(
135                                                                        "Unhandled exception thrown by user code in task " + name, e);
136                                                        }
137
138                                                        if (log.isTraceEnabled())
139                                                        {
140                                                                log.trace("Finished with job: '{}'", code);
141                                                        }
142
143                                                        // Sleep until the period is over (or not at all if it's
144                                                        // already passed)
145                                                        Instant nextExecution = startOfPeriod.plus(frequency);
146                                                        
147                                                        Duration timeToNextExecution = Duration.between(Instant.now(), nextExecution);
148
149                                                        if (!timeToNextExecution.isNegative())
150                                                        {
151                                                                try {
152                                                                        Thread.sleep(timeToNextExecution.toMillis());
153                                                                }
154                                                                catch (InterruptedException e) {
155                                                                        Thread.currentThread().interrupt();
156                                                                }
157                                                        }
158                                                }
159                                                log.trace("Task '{}' stopped", name);
160                                        }
161                                        catch (Exception x)
162                                        {
163                                                log.error("Task '{}' terminated", name, x);
164                                        }
165                                        finally
166                                        {
167                                                isStarted = false;
168                                        }
169                                }
170                        };
171
172                        // Start the thread
173                        thread = new Thread(runnable, name + " Task");
174                        thread.setDaemon(isDaemon);
175                        thread.start();
176
177                        // We're started all right!
178                        isStarted = true;
179                }
180                else
181                {
182                        throw new IllegalStateException("Attempt to start task that has already been started");
183                }
184        }
185
186        /**
187         * Sets daemon or not. For obvious reasons, this value can only be set before the task starts
188         * running. If you attempt to set this value after the task starts running, an
189         * <code>IllegalStateException</code> will be thrown.
190         * 
191         * @param daemon
192         *            <code>true</code> if this <code>Task</code>'s <code>Thread</code> should be a
193         *            daemon
194         * @throws IllegalStateException
195         *             thrown if task is already running
196         */
197        public synchronized void setDaemon(final boolean daemon)
198        {
199                if (isStarted)
200                {
201                        throw new IllegalStateException(
202                                "Attempt to set daemon state of a task that has already been started");
203                }
204
205                isDaemon = daemon;
206        }
207
208        /**
209         * Sets log for user code to log to when task runs.
210         * 
211         * @param log
212         *            the log
213         */
214        public synchronized void setLog(final Logger log)
215        {
216                this.log = log;
217        }
218
219        /**
220         * Sets start time for this task. You cannot set the start time for a task which is already
221         * running. If you attempt to, an IllegalStateException will be thrown.
222         * 
223         * @param startTime
224         *            The time this task should start running
225         * @throws IllegalStateException
226         *             Thrown if task is already running
227         */
228        public synchronized void setStartTime(final Instant startTime)
229        {
230                if (isStarted)
231                {
232                        throw new IllegalStateException(
233                                "Attempt to set start time of task that has already been started");
234                }
235
236                this.startTime = startTime;
237        }
238
239        /**
240         * @see java.lang.Object#toString()
241         */
242        @Override
243        public String toString()
244        {
245                return "[name=" + name + ", startTime=" + startTime + ", isDaemon=" + isDaemon +
246                        ", isStarted=" + isStarted + ", codeListener=" + log + "]";
247        }
248
249        /**
250         * Gets the log for this <code>Task</code>.
251         * 
252         * @return the log
253         */
254        protected synchronized Logger getLog()
255        {
256                if (log == null)
257                {
258                        log = LoggerFactory.getLogger(Task.class);
259                }
260                return log;
261        }
262
263        /**
264         * Stops this <code>Task</code> as soon as it has the opportunity.
265         */
266        public void stop()
267        {
268                stop = true;
269        }
270
271        /**
272         * Interrupts the <code>Task</code> as soon as it has the opportunity.
273         */
274        public void interrupt()
275        {
276                stop();
277                if (thread != null)
278                {
279                        thread.interrupt();
280                }
281        }
282
283        /**
284         * Sets the priority of the thread
285         * 
286         * @param prio
287         */
288        public void setPriority(int prio)
289        {
290                if (prio < Thread.MIN_PRIORITY)
291                {
292                        prio = Thread.MIN_PRIORITY;
293                }
294                else if (prio > Thread.MAX_PRIORITY)
295                {
296                        prio = Thread.MAX_PRIORITY;
297                }
298                thread.setPriority(prio);
299        }
300
301        /**
302         * Gets the thread priority
303         * 
304         * @return priority
305         */
306        public int getPriority()
307        {
308                return thread.getPriority();
309        }
310}