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}