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                                                                Thread.sleep(timeToNextExecution.toMillis());
152                                                        }
153                                                        
154                                                }
155                                        }
156                                        catch (Exception x)
157                                        {
158                                                log.error("Task '{}' terminated", name, x);
159                                        }
160                                        finally
161                                        {
162                                                isStarted = false;
163                                        }
164                                }
165                        };
166
167                        // Start the thread
168                        thread = new Thread(runnable, name + " Task");
169                        thread.setDaemon(isDaemon);
170                        thread.start();
171
172                        // We're started all right!
173                        isStarted = true;
174                }
175                else
176                {
177                        throw new IllegalStateException("Attempt to start task that has already been started");
178                }
179        }
180
181        /**
182         * Sets daemon or not. For obvious reasons, this value can only be set before the task starts
183         * running. If you attempt to set this value after the task starts running, an
184         * <code>IllegalStateException</code> will be thrown.
185         * 
186         * @param daemon
187         *            <code>true</code> if this <code>Task</code>'s <code>Thread</code> should be a
188         *            daemon
189         * @throws IllegalStateException
190         *             thrown if task is already running
191         */
192        public synchronized void setDaemon(final boolean daemon)
193        {
194                if (isStarted)
195                {
196                        throw new IllegalStateException(
197                                "Attempt to set daemon state of a task that has already been started");
198                }
199
200                isDaemon = daemon;
201        }
202
203        /**
204         * Sets log for user code to log to when task runs.
205         * 
206         * @param log
207         *            the log
208         */
209        public synchronized void setLog(final Logger log)
210        {
211                this.log = log;
212        }
213
214        /**
215         * Sets start time for this task. You cannot set the start time for a task which is already
216         * running. If you attempt to, an IllegalStateException will be thrown.
217         * 
218         * @param startTime
219         *            The time this task should start running
220         * @throws IllegalStateException
221         *             Thrown if task is already running
222         */
223        public synchronized void setStartTime(final Instant startTime)
224        {
225                if (isStarted)
226                {
227                        throw new IllegalStateException(
228                                "Attempt to set start time of task that has already been started");
229                }
230
231                this.startTime = startTime;
232        }
233
234        /**
235         * @see java.lang.Object#toString()
236         */
237        @Override
238        public String toString()
239        {
240                return "[name=" + name + ", startTime=" + startTime + ", isDaemon=" + isDaemon +
241                        ", isStarted=" + isStarted + ", codeListener=" + log + "]";
242        }
243
244        /**
245         * Gets the log for this <code>Task</code>.
246         * 
247         * @return the log
248         */
249        protected synchronized Logger getLog()
250        {
251                if (log == null)
252                {
253                        log = LoggerFactory.getLogger(Task.class);
254                }
255                return log;
256        }
257
258        /**
259         * Stops this <code>Task</code> as soon as it has the opportunity.
260         */
261        public void stop()
262        {
263                stop = true;
264        }
265
266        /**
267         * Interrupts the <code>Task</code> as soon as it has the opportunity.
268         */
269        public void interrupt()
270        {
271                stop();
272                if (thread != null)
273                {
274                        thread.interrupt();
275                }
276        }
277
278        /**
279         * Sets the priority of the thread
280         * 
281         * @param prio
282         */
283        public void setPriority(int prio)
284        {
285                if (prio < Thread.MIN_PRIORITY)
286                {
287                        prio = Thread.MIN_PRIORITY;
288                }
289                else if (prio > Thread.MAX_PRIORITY)
290                {
291                        prio = Thread.MAX_PRIORITY;
292                }
293                thread.setPriority(prio);
294        }
295
296        /**
297         * Gets the thread priority
298         * 
299         * @return priority
300         */
301        public int getPriority()
302        {
303                return thread.getPriority();
304        }
305}