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.activemq.store.kahadb.scheduler; 018 019import java.io.DataInput; 020import java.io.DataOutput; 021import java.io.IOException; 022import java.util.ArrayList; 023import java.util.HashMap; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Map; 027import java.util.concurrent.CopyOnWriteArrayList; 028import java.util.concurrent.atomic.AtomicBoolean; 029 030import javax.jms.MessageFormatException; 031 032import org.apache.activemq.broker.scheduler.CronParser; 033import org.apache.activemq.broker.scheduler.Job; 034import org.apache.activemq.broker.scheduler.JobListener; 035import org.apache.activemq.broker.scheduler.JobScheduler; 036import org.apache.activemq.protobuf.Buffer; 037import org.apache.activemq.store.kahadb.data.KahaAddScheduledJobCommand; 038import org.apache.activemq.store.kahadb.data.KahaRemoveScheduledJobCommand; 039import org.apache.activemq.store.kahadb.data.KahaRemoveScheduledJobsCommand; 040import org.apache.activemq.store.kahadb.data.KahaRescheduleJobCommand; 041import org.apache.activemq.store.kahadb.disk.index.BTreeIndex; 042import org.apache.activemq.store.kahadb.disk.journal.Location; 043import org.apache.activemq.store.kahadb.disk.page.Transaction; 044import org.apache.activemq.store.kahadb.disk.util.LongMarshaller; 045import org.apache.activemq.util.ByteSequence; 046import org.apache.activemq.util.IdGenerator; 047import org.apache.activemq.util.ServiceStopper; 048import org.apache.activemq.util.ServiceSupport; 049import org.slf4j.Logger; 050import org.slf4j.LoggerFactory; 051 052public class JobSchedulerImpl extends ServiceSupport implements Runnable, JobScheduler { 053 054 private static final Logger LOG = LoggerFactory.getLogger(JobSchedulerImpl.class); 055 private final JobSchedulerStoreImpl store; 056 private final AtomicBoolean running = new AtomicBoolean(); 057 private String name; 058 private BTreeIndex<Long, List<JobLocation>> index; 059 private Thread thread; 060 private final AtomicBoolean started = new AtomicBoolean(false); 061 private final List<JobListener> jobListeners = new CopyOnWriteArrayList<>(); 062 private static final IdGenerator ID_GENERATOR = new IdGenerator(); 063 private final ScheduleTime scheduleTime = new ScheduleTime(); 064 065 JobSchedulerImpl(JobSchedulerStoreImpl store) { 066 this.store = store; 067 } 068 069 public void setName(String name) { 070 this.name = name; 071 } 072 073 @Override 074 public String getName() { 075 return this.name; 076 } 077 078 @Override 079 public void addListener(JobListener l) { 080 this.jobListeners.add(l); 081 } 082 083 @Override 084 public void removeListener(JobListener l) { 085 this.jobListeners.remove(l); 086 } 087 088 @Override 089 public void schedule(final String jobId, final ByteSequence payload, final long delay) throws IOException { 090 doSchedule(jobId, payload, "", 0, delay, 0); 091 } 092 093 @Override 094 public void schedule(final String jobId, final ByteSequence payload, final String cronEntry) throws Exception { 095 doSchedule(jobId, payload, cronEntry, 0, 0, 0); 096 } 097 098 @Override 099 public void schedule(final String jobId, final ByteSequence payload, final String cronEntry, final long delay, final long period, final int repeat) throws IOException { 100 doSchedule(jobId, payload, cronEntry, delay, period, repeat); 101 } 102 103 @Override 104 public void remove(final long time) throws IOException { 105 doRemoveRange(time, time); 106 } 107 108 @Override 109 public void remove(final String jobId) throws IOException { 110 doRemove(-1, jobId); 111 } 112 113 @Override 114 public void removeAllJobs() throws IOException { 115 doRemoveRange(0, Long.MAX_VALUE); 116 } 117 118 @Override 119 public void removeAllJobs(final long start, final long finish) throws IOException { 120 doRemoveRange(start, finish); 121 } 122 123 @Override 124 public long getNextScheduleTime() throws IOException { 125 this.store.readLockIndex(); 126 try { 127 Map.Entry<Long, List<JobLocation>> first = this.index.getFirst(this.store.getPageFile().tx()); 128 return first != null ? first.getKey() : -1l; 129 } finally { 130 this.store.readUnlockIndex(); 131 } 132 } 133 134 @Override 135 public List<Job> getNextScheduleJobs() throws IOException { 136 final List<Job> result = new ArrayList<>(); 137 this.store.readLockIndex(); 138 try { 139 this.store.getPageFile().tx().execute(new Transaction.Closure<IOException>() { 140 @Override 141 public void execute(Transaction tx) throws IOException { 142 Map.Entry<Long, List<JobLocation>> first = index.getFirst(tx); 143 if (first != null) { 144 for (JobLocation jl : first.getValue()) { 145 ByteSequence bs = getPayload(jl.getLocation()); 146 Job job = new JobImpl(jl, bs); 147 result.add(job); 148 } 149 } 150 } 151 }); 152 } finally { 153 this.store.readUnlockIndex(); 154 } 155 return result; 156 } 157 158 @Override 159 public List<Job> getAllJobs() throws IOException { 160 final List<Job> result = new ArrayList<>(); 161 this.store.readLockIndex(); 162 try { 163 this.store.getPageFile().tx().execute(new Transaction.Closure<IOException>() { 164 @Override 165 public void execute(Transaction tx) throws IOException { 166 Iterator<Map.Entry<Long, List<JobLocation>>> iter = index.iterator(store.getPageFile().tx()); 167 while (iter.hasNext()) { 168 Map.Entry<Long, List<JobLocation>> next = iter.next(); 169 if (next != null) { 170 for (JobLocation jl : next.getValue()) { 171 ByteSequence bs = getPayload(jl.getLocation()); 172 Job job = new JobImpl(jl, bs); 173 result.add(job); 174 } 175 } else { 176 break; 177 } 178 } 179 } 180 }); 181 } finally { 182 this.store.readUnlockIndex(); 183 } 184 return result; 185 } 186 187 @Override 188 public List<Job> getAllJobs(final long start, final long finish) throws IOException { 189 final List<Job> result = new ArrayList<>(); 190 this.store.readLockIndex(); 191 try { 192 this.store.getPageFile().tx().execute(new Transaction.Closure<IOException>() { 193 @Override 194 public void execute(Transaction tx) throws IOException { 195 Iterator<Map.Entry<Long, List<JobLocation>>> iter = index.iterator(tx, start); 196 while (iter.hasNext()) { 197 Map.Entry<Long, List<JobLocation>> next = iter.next(); 198 if (next != null && next.getKey().longValue() <= finish) { 199 for (JobLocation jl : next.getValue()) { 200 ByteSequence bs = getPayload(jl.getLocation()); 201 Job job = new JobImpl(jl, bs); 202 result.add(job); 203 } 204 } else { 205 break; 206 } 207 } 208 } 209 }); 210 } finally { 211 this.store.readUnlockIndex(); 212 } 213 return result; 214 } 215 216 private void doSchedule(final String jobId, final ByteSequence payload, final String cronEntry, long delay, long period, int repeat) throws IOException { 217 long startTime = System.currentTimeMillis(); 218 // round startTime - so we can schedule more jobs at the same time 219 startTime = ((startTime + 500) / 500) * 500; 220 221 long time = 0; 222 if (cronEntry != null && cronEntry.length() > 0) { 223 try { 224 time = CronParser.getNextScheduledTime(cronEntry, startTime); 225 } catch (MessageFormatException e) { 226 throw new IOException(e.getMessage()); 227 } 228 } 229 230 if (time == 0) { 231 // start time not set by CRON - so it it to the current time 232 time = startTime; 233 } 234 235 if (delay > 0) { 236 time += delay; 237 } else { 238 time += period; 239 } 240 241 KahaAddScheduledJobCommand newJob = new KahaAddScheduledJobCommand(); 242 newJob.setScheduler(name); 243 newJob.setJobId(jobId); 244 newJob.setStartTime(startTime); 245 newJob.setCronEntry(cronEntry); 246 newJob.setDelay(delay); 247 newJob.setPeriod(period); 248 newJob.setRepeat(repeat); 249 newJob.setNextExecutionTime(time); 250 newJob.setPayload(new Buffer(payload.getData(), payload.getOffset(), payload.getLength())); 251 252 this.store.store(newJob); 253 } 254 255 private void doReschedule(List<Closure> toReschedule) throws IOException { 256 for (Closure closure : toReschedule) { 257 closure.run(); 258 } 259 } 260 261 private void doReschedule(final String jobId, long executionTime, long nextExecutionTime, int rescheduledCount) throws IOException { 262 KahaRescheduleJobCommand update = new KahaRescheduleJobCommand(); 263 update.setScheduler(name); 264 update.setJobId(jobId); 265 update.setExecutionTime(executionTime); 266 update.setNextExecutionTime(nextExecutionTime); 267 update.setRescheduledCount(rescheduledCount); 268 this.store.store(update); 269 } 270 271 private void doRemove(final List<Closure> toRemove) throws IOException { 272 for (Closure closure : toRemove) { 273 closure.run(); 274 } 275 } 276 277 private void doRemove(long executionTime, final String jobId) throws IOException { 278 KahaRemoveScheduledJobCommand remove = new KahaRemoveScheduledJobCommand(); 279 remove.setScheduler(name); 280 remove.setJobId(jobId); 281 remove.setNextExecutionTime(executionTime); 282 this.store.store(remove); 283 } 284 285 private void doRemoveRange(long start, long end) throws IOException { 286 KahaRemoveScheduledJobsCommand destroy = new KahaRemoveScheduledJobsCommand(); 287 destroy.setScheduler(name); 288 destroy.setStartTime(start); 289 destroy.setEndTime(end); 290 this.store.store(destroy); 291 } 292 293 /** 294 * Adds a new Scheduled job to the index. Must be called under index lock. 295 * 296 * This method must ensure that a duplicate add is not processed into the scheduler. On index 297 * recover some adds may be replayed and we don't allow more than one instance of a JobId to 298 * exist at any given scheduled time, so filter these out to ensure idempotence. 299 * 300 * @param tx 301 * Transaction in which the update is performed. 302 * @param command 303 * The new scheduled job command to process. 304 * @param location 305 * The location where the add command is stored in the journal. 306 * 307 * @throws IOException if an error occurs updating the index. 308 */ 309 protected void process(final Transaction tx, final KahaAddScheduledJobCommand command, Location location) throws IOException { 310 JobLocation jobLocation = new JobLocation(location); 311 jobLocation.setJobId(command.getJobId()); 312 jobLocation.setStartTime(command.getStartTime()); 313 jobLocation.setCronEntry(command.getCronEntry()); 314 jobLocation.setDelay(command.getDelay()); 315 jobLocation.setPeriod(command.getPeriod()); 316 jobLocation.setRepeat(command.getRepeat()); 317 318 long nextExecutionTime = command.getNextExecutionTime(); 319 320 List<JobLocation> values = null; 321 jobLocation.setNextTime(nextExecutionTime); 322 if (this.index.containsKey(tx, nextExecutionTime)) { 323 values = this.index.remove(tx, nextExecutionTime); 324 } 325 if (values == null) { 326 values = new ArrayList<>(); 327 } 328 329 // There can never be more than one instance of the same JobId scheduled at any 330 // given time, when it happens its probably the result of index recovery and this 331 // method must be idempotent so check for it first. 332 if (!values.contains(jobLocation)) { 333 values.add(jobLocation); 334 335 // Reference the log file where the add command is stored to prevent GC. 336 this.store.incrementJournalCount(tx, location); 337 this.index.put(tx, nextExecutionTime, values); 338 this.scheduleTime.newJob(); 339 } else { 340 this.index.put(tx, nextExecutionTime, values); 341 LOG.trace("Job {} already in scheduler at this time {}", 342 jobLocation.getJobId(), jobLocation.getNextTime()); 343 } 344 } 345 346 /** 347 * Reschedules a Job after it has be fired. 348 * 349 * For jobs that are repeating this method updates the job in the index by adding it to the 350 * jobs list for the new execution time. If the job is not a cron type job then this method 351 * will reduce the repeat counter if the job has a fixed number of repeats set. The Job will 352 * be removed from the jobs list it just executed on. 353 * 354 * This method must also update the value of the last update location in the JobLocation 355 * instance so that the checkpoint worker doesn't drop the log file in which that command lives. 356 * 357 * This method must ensure that an reschedule command that references a job that doesn't exist 358 * does not cause an error since it's possible that on recover the original add might be gone 359 * and so the job should not reappear in the scheduler. 360 * 361 * @param tx 362 * The TX under which the index is updated. 363 * @param command 364 * The reschedule command to process. 365 * @param location 366 * The location in the index where the reschedule command was stored. 367 * 368 * @throws IOException if an error occurs during the reschedule. 369 */ 370 protected void process(final Transaction tx, final KahaRescheduleJobCommand command, Location location) throws IOException { 371 JobLocation result = null; 372 final List<JobLocation> current = this.index.remove(tx, command.getExecutionTime()); 373 if (current != null) { 374 for (int i = 0; i < current.size(); i++) { 375 JobLocation jl = current.get(i); 376 if (jl.getJobId().equals(command.getJobId())) { 377 current.remove(i); 378 if (!current.isEmpty()) { 379 this.index.put(tx, command.getExecutionTime(), current); 380 } 381 result = jl; 382 break; 383 } 384 } 385 } else { 386 LOG.debug("Process reschedule command for job {} non-existent executime time {}.", 387 command.getJobId(), command.getExecutionTime()); 388 } 389 390 if (result != null) { 391 Location previousUpdate = result.getLastUpdate(); 392 393 List<JobLocation> target = null; 394 result.setNextTime(command.getNextExecutionTime()); 395 result.setLastUpdate(location); 396 result.setRescheduledCount(command.getRescheduledCount()); 397 if (!result.isCron() && result.getRepeat() > 0) { 398 result.setRepeat(result.getRepeat() - 1); 399 } 400 if (this.index.containsKey(tx, command.getNextExecutionTime())) { 401 target = this.index.remove(tx, command.getNextExecutionTime()); 402 } 403 if (target == null) { 404 target = new ArrayList<>(); 405 } 406 target.add(result); 407 408 // Track the location of the last reschedule command and release the log file 409 // reference for the previous one if there was one. 410 this.store.incrementJournalCount(tx, location); 411 if (previousUpdate != null) { 412 this.store.decrementJournalCount(tx, previousUpdate); 413 } 414 415 this.index.put(tx, command.getNextExecutionTime(), target); 416 this.scheduleTime.newJob(); 417 } else { 418 LOG.debug("Process reschedule command for non-scheduled job {} at executime time {}.", 419 command.getJobId(), command.getExecutionTime()); 420 } 421 } 422 423 /** 424 * Removes a scheduled job from the scheduler. 425 * 426 * The remove operation can be of two forms. The first is that there is a job Id but no set time 427 * (-1) in which case the jobs index is searched until the target job Id is located. The alternate 428 * form is that a job Id and execution time are both set in which case the given time is checked 429 * for a job matching that Id. In either case once an execution time is identified the job is 430 * removed and the index updated. 431 * 432 * This method should ensure that if the matching job is not found that no error results as it 433 * is possible that on a recover the initial add command could be lost so the job may not be 434 * rescheduled. 435 * 436 * @param tx 437 * The transaction under which the index is updated. 438 * @param command 439 * The remove command to process. 440 * @param location 441 * The location of the remove command in the Journal. 442 * 443 * @throws IOException if an error occurs while updating the scheduler index. 444 */ 445 void process(final Transaction tx, final KahaRemoveScheduledJobCommand command, Location location) throws IOException { 446 447 // Case 1: JobId and no time value means find the job and remove it. 448 // Case 2: JobId and a time value means find exactly this scheduled job. 449 450 Long executionTime = command.getNextExecutionTime(); 451 452 List<JobLocation> values = null; 453 454 if (executionTime == -1) { 455 for (Iterator<Map.Entry<Long, List<JobLocation>>> i = this.index.iterator(tx); i.hasNext();) { 456 Map.Entry<Long, List<JobLocation>> entry = i.next(); 457 List<JobLocation> candidates = entry.getValue(); 458 if (candidates != null) { 459 for (JobLocation jl : candidates) { 460 if (jl.getJobId().equals(command.getJobId())) { 461 LOG.trace("Entry {} contains the remove target: {}", entry.getKey(), command.getJobId()); 462 executionTime = entry.getKey(); 463 values = this.index.remove(tx, executionTime); 464 break; 465 } 466 } 467 } 468 } 469 } else { 470 values = this.index.remove(tx, executionTime); 471 } 472 473 JobLocation removed = null; 474 475 // Remove the job and update the index if there are any other jobs scheduled at this time. 476 if (values != null) { 477 for (JobLocation job : values) { 478 if (job.getJobId().equals(command.getJobId())) { 479 removed = job; 480 values.remove(removed); 481 break; 482 } 483 } 484 485 if (!values.isEmpty()) { 486 this.index.put(tx, executionTime, values); 487 } 488 } 489 490 if (removed != null) { 491 LOG.trace("{} removed from scheduler {}", removed, this); 492 493 // Remove the references for add and reschedule commands for this job 494 // so that those logs can be GC'd when free. 495 this.store.decrementJournalCount(tx, removed.getLocation()); 496 if (removed.getLastUpdate() != null) { 497 this.store.decrementJournalCount(tx, removed.getLastUpdate()); 498 } 499 500 // now that the job is removed from the index we can store the remove info and 501 // then dereference the log files that hold the initial add command and the most 502 // recent update command. If the remove and the add that created the job are in 503 // the same file we don't need to track it and just let a normal GC of the logs 504 // remove it when the log is unreferenced. 505 if (removed.getLocation().getDataFileId() != location.getDataFileId()) { 506 this.store.referenceRemovedLocation(tx, location, removed); 507 } 508 } 509 } 510 511 /** 512 * Removes all scheduled jobs within a given time range. 513 * 514 * The method can be used to clear the entire scheduler index by specifying a range that 515 * encompasses all time [0...Long.MAX_VALUE] or a single execution time can be removed by 516 * setting start and end time to the same value. 517 * 518 * @param tx 519 * The transaction under which the index is updated. 520 * @param command 521 * The remove command to process. 522 * @param location 523 * The location of the remove command in the Journal. 524 * 525 * @throws IOException if an error occurs while updating the scheduler index. 526 */ 527 protected void process(final Transaction tx, final KahaRemoveScheduledJobsCommand command, Location location) throws IOException { 528 removeInRange(tx, command.getStartTime(), command.getEndTime(), location); 529 } 530 531 /** 532 * Removes all jobs from the schedulers index. Must be called with the index locked. 533 * 534 * @param tx 535 * The transaction under which the index entries for this scheduler are removed. 536 * 537 * @throws IOException if an error occurs removing the jobs from the scheduler index. 538 */ 539 protected void removeAll(Transaction tx) throws IOException { 540 this.removeInRange(tx, 0, Long.MAX_VALUE, null); 541 } 542 543 /** 544 * Removes all scheduled jobs within the target range. 545 * 546 * This method can be used to remove all the stored jobs by passing a range of [0...Long.MAX_VALUE] 547 * or it can be used to remove all jobs at a given scheduled time by passing the same time value 548 * for both start and end. If the optional location parameter is set then this method will update 549 * the store's remove location tracker with the location value and the Jobs that are being removed. 550 * 551 * This method must be called with the store index locked for writes. 552 * 553 * @param tx 554 * The transaction under which the index is to be updated. 555 * @param start 556 * The start time for the remove operation. 557 * @param finish 558 * The end time for the remove operation. 559 * @param location (optional) 560 * The location of the remove command that triggered this remove. 561 * 562 * @throws IOException if an error occurs during the remove operation. 563 */ 564 protected void removeInRange(Transaction tx, long start, long finish, Location location) throws IOException { 565 List<Long> keys = new ArrayList<>(); 566 for (Iterator<Map.Entry<Long, List<JobLocation>>> i = this.index.iterator(tx, start); i.hasNext();) { 567 Map.Entry<Long, List<JobLocation>> entry = i.next(); 568 if (entry.getKey().longValue() <= finish) { 569 keys.add(entry.getKey()); 570 } else { 571 break; 572 } 573 } 574 575 List<Integer> removedJobFileIds = new ArrayList<>(); 576 HashMap<Integer, Integer> decrementJournalCount = new HashMap<>(); 577 578 for (Long executionTime : keys) { 579 List<JobLocation> values = this.index.remove(tx, executionTime); 580 if (location != null) { 581 for (JobLocation job : values) { 582 LOG.trace("Removing {} scheduled at: {}", job, executionTime); 583 584 // Remove the references for add and reschedule commands for this job 585 // so that those logs can be GC'd when free. 586 decrementJournalCount.compute(job.getLocation().getDataFileId(), (key, value) -> value == null ? 1 : value + 1); 587 if (job.getLastUpdate() != null) { 588 decrementJournalCount.compute(job.getLastUpdate().getDataFileId(), (key, value) -> value == null ? 1 : value + 1); 589 } 590 591 // now that the job is removed from the index we can store the remove info and 592 // then dereference the log files that hold the initial add command and the most 593 // recent update command. If the remove and the add that created the job are in 594 // the same file we don't need to track it and just let a normal GC of the logs 595 // remove it when the log is unreferenced. 596 if (job.getLocation().getDataFileId() != location.getDataFileId()) { 597 removedJobFileIds.add(job.getLocation().getDataFileId()); 598 } 599 } 600 } 601 } 602 603 if (!removedJobFileIds.isEmpty()) { 604 this.store.referenceRemovedLocation(tx, location, removedJobFileIds); 605 } 606 607 if (decrementJournalCount.size() > 0) { 608 this.store.decrementJournalCount(tx, decrementJournalCount); 609 } 610 } 611 612 /** 613 * Removes a Job from the index using it's Id value and the time it is currently set to 614 * be executed. This method will only remove the Job if it is found at the given execution 615 * time. 616 * 617 * This method must be called under index lock. 618 * 619 * @param tx 620 * the transaction under which this method is being executed. 621 * @param jobId 622 * the target Job Id to remove. 623 * @param executionTime 624 * the scheduled time that for the Job Id that is being removed. 625 * 626 * @returns true if the Job was removed or false if not found at the given time. 627 * 628 * @throws IOException if an error occurs while removing the Job. 629 */ 630 protected boolean removeJobAtTime(Transaction tx, String jobId, long executionTime) throws IOException { 631 boolean result = false; 632 633 List<JobLocation> jobs = this.index.remove(tx, executionTime); 634 Iterator<JobLocation> jobsIter = jobs.iterator(); 635 while (jobsIter.hasNext()) { 636 JobLocation job = jobsIter.next(); 637 if (job.getJobId().equals(jobId)) { 638 jobsIter.remove(); 639 // Remove the references for add and reschedule commands for this job 640 // so that those logs can be GC'd when free. 641 this.store.decrementJournalCount(tx, job.getLocation()); 642 if (job.getLastUpdate() != null) { 643 this.store.decrementJournalCount(tx, job.getLastUpdate()); 644 } 645 result = true; 646 break; 647 } 648 } 649 650 // Return the list to the index modified or unmodified. 651 this.index.put(tx, executionTime, jobs); 652 653 return result; 654 } 655 656 /** 657 * Walks the Scheduled Job Tree and collects the add location and last update location 658 * for all scheduled jobs. 659 * 660 * This method must be called with the index locked. 661 * 662 * @param tx 663 * the transaction under which this operation was invoked. 664 * 665 * @return a iterator of all referenced Location values for this JobSchedulerImpl 666 * 667 * @throws IOException if an error occurs walking the scheduler tree. 668 */ 669 protected Iterator<JobLocation> getAllScheduledJobs(Transaction tx) throws IOException { 670 return new Iterator<JobLocation>() { 671 672 final Iterator<Map.Entry<Long, List<JobLocation>>> mapIterator = index.iterator(tx); 673 Iterator<JobLocation> iterator; 674 675 @Override 676 public boolean hasNext() { 677 678 while (iterator == null || !iterator.hasNext()) { 679 if (!mapIterator.hasNext()) { 680 break; 681 } 682 683 iterator = new ArrayList<>(mapIterator.next().getValue()).iterator(); 684 } 685 686 return iterator != null && iterator.hasNext(); 687 } 688 689 @Override 690 public JobLocation next() { 691 return iterator.next(); 692 } 693 }; 694 } 695 696 @Override 697 public void run() { 698 try { 699 mainLoop(); 700 } catch (Throwable e) { 701 if (this.running.get() && isStarted()) { 702 LOG.error("{} Caught exception in mainloop", this, e); 703 } 704 } finally { 705 if (running.get()) { 706 try { 707 stop(); 708 } catch (Exception e) { 709 LOG.error("Failed to stop {}", this); 710 } 711 } 712 } 713 } 714 715 @Override 716 public String toString() { 717 return "JobScheduler: " + this.name; 718 } 719 720 protected void mainLoop() { 721 while (this.running.get()) { 722 this.scheduleTime.clearNewJob(); 723 try { 724 long currentTime = System.currentTimeMillis(); 725 726 // Read the list of scheduled events and fire the jobs, reschedule repeating jobs as 727 // needed before firing the job event. 728 List<Closure> toRemove = new ArrayList<>(); 729 List<Closure> toReschedule = new ArrayList<>(); 730 try { 731 this.store.readLockIndex(); 732 733 Iterator<Map.Entry<Long, List<JobLocation>>> iterator = this.index.iterator(this.store.getPageFile().tx()); 734 while (iterator.hasNext()) { 735 Map.Entry<Long, List<JobLocation>> next = iterator.next(); 736 if (next != null) { 737 List<JobLocation> list = new ArrayList<>(next.getValue()); 738 final long executionTime = next.getKey(); 739 long nextExecutionTime = 0; 740 741 if (executionTime <= currentTime) { 742 for (final JobLocation job : list) { 743 744 if (!running.get()) { 745 break; 746 } 747 748 int repeat = job.getRepeat(); 749 nextExecutionTime = calculateNextExecutionTime(job, currentTime, repeat); 750 long waitTime = nextExecutionTime - currentTime; 751 this.scheduleTime.setWaitTime(waitTime); 752 if (!job.isCron()) { 753 fireJob(job); 754 if (repeat != 0) { 755 // Reschedule for the next time, the scheduler will take care of 756 // updating the repeat counter on the update. 757 final long finalNextExecutionTime = nextExecutionTime; 758 toReschedule.add(() -> doReschedule(job.getJobId(), executionTime, finalNextExecutionTime, job.getRescheduledCount() + 1)); 759 } else { 760 toRemove.add(() -> doRemove(executionTime, job.getJobId())); 761 } 762 } else { 763 if (repeat == 0) { 764 // This is a non-repeating Cron entry so we can fire and forget it. 765 fireJob(job); 766 } 767 768 if (nextExecutionTime > currentTime) { 769 // Reschedule the cron job as a new event, if the cron entry signals 770 // a repeat then it will be stored separately and fired as a normal 771 // event with decrementing repeat. 772 final long finalNextExecutionTime = nextExecutionTime; 773 toReschedule.add(() -> doReschedule(job.getJobId(), executionTime, finalNextExecutionTime, job.getRescheduledCount() + 1)); 774 775 if (repeat != 0) { 776 // we have a separate schedule to run at this time 777 // so the cron job is used to set of a separate schedule 778 // hence we won't fire the original cron job to the 779 // listeners but we do need to start a separate schedule 780 String jobId = ID_GENERATOR.generateId(); 781 ByteSequence payload = getPayload(job.getLocation()); 782 schedule(jobId, payload, "", job.getDelay(), job.getPeriod(), job.getRepeat()); 783 waitTime = job.getDelay() != 0 ? job.getDelay() : job.getPeriod(); 784 this.scheduleTime.setWaitTime(waitTime); 785 } 786 } else { 787 toRemove.add(() -> doRemove(executionTime, job.getJobId())); 788 } 789 } 790 } 791 } else { 792 this.scheduleTime.setWaitTime(executionTime - currentTime); 793 break; 794 } 795 } 796 } 797 } finally { 798 this.store.readUnlockIndex(); 799 800 doReschedule(toReschedule); 801 802 // now remove all jobs that have not been rescheduled, 803 // if there are no more entries in that time it will be removed. 804 doRemove(toRemove); 805 } 806 807 this.scheduleTime.pause(); 808 } catch (Exception ioe) { 809 LOG.error("{} Failed to schedule job", this.name, ioe); 810 try { 811 this.store.stop(); 812 } catch (Exception e) { 813 LOG.error("{} Failed to shutdown JobSchedulerStore", this.name, e); 814 } 815 } 816 } 817 } 818 819 void fireJob(JobLocation job) throws IllegalStateException, IOException { 820 LOG.debug("Firing: {}", job); 821 ByteSequence bs = this.store.getPayload(job.getLocation()); 822 for (JobListener l : jobListeners) { 823 l.scheduledJob(job.getJobId(), bs); 824 } 825 } 826 827 @Override 828 public void startDispatching() throws Exception { 829 if (!this.running.get()) { 830 return; 831 } 832 833 if (started.compareAndSet(false, true)) { 834 this.thread = new Thread(this, "JobScheduler:" + this.name); 835 this.thread.setDaemon(true); 836 this.thread.start(); 837 } 838 } 839 840 @Override 841 public void stopDispatching() throws Exception { 842 if (started.compareAndSet(true, false)) { 843 this.scheduleTime.wakeup(); 844 Thread t = this.thread; 845 this.thread = null; 846 if (t != null) { 847 t.join(3000); 848 } 849 } 850 } 851 852 @Override 853 protected void doStart() throws Exception { 854 this.running.set(true); 855 } 856 857 @Override 858 protected void doStop(ServiceStopper stopper) throws Exception { 859 this.running.set(false); 860 stopDispatching(); 861 } 862 863 private ByteSequence getPayload(Location location) throws IllegalStateException, IOException { 864 return this.store.getPayload(location); 865 } 866 867 long calculateNextExecutionTime(final JobLocation job, long currentTime, int repeat) throws MessageFormatException { 868 long result = currentTime; 869 String cron = job.getCronEntry(); 870 if (cron != null && cron.length() > 0) { 871 result = CronParser.getNextScheduledTime(cron, result); 872 } else if (job.getRepeat() != 0) { 873 result += job.getPeriod(); 874 } 875 return result; 876 } 877 878 void createIndexes(Transaction tx) throws IOException { 879 this.index = new BTreeIndex<>(this.store.getPageFile(), tx.allocate().getPageId()); 880 } 881 882 void load(Transaction tx) throws IOException { 883 this.index.setKeyMarshaller(LongMarshaller.INSTANCE); 884 this.index.setValueMarshaller(JobLocationsMarshaller.INSTANCE); 885 this.index.load(tx); 886 } 887 888 void read(DataInput in) throws IOException { 889 this.name = in.readUTF(); 890 this.index = new BTreeIndex<>(this.store.getPageFile(), in.readLong()); 891 this.index.setKeyMarshaller(LongMarshaller.INSTANCE); 892 this.index.setValueMarshaller(JobLocationsMarshaller.INSTANCE); 893 } 894 895 public void write(DataOutput out) throws IOException { 896 out.writeUTF(name); 897 out.writeLong(this.index.getPageId()); 898 } 899 900 private interface Closure { 901 void run() throws IOException; 902 } 903 904 static class ScheduleTime { 905 private final int DEFAULT_WAIT = 500; 906 private final int DEFAULT_NEW_JOB_WAIT = 100; 907 private boolean newJob; 908 private long waitTime = DEFAULT_WAIT; 909 private final Object mutex = new Object(); 910 911 /** 912 * @return the waitTime 913 */ 914 long getWaitTime() { 915 return this.waitTime; 916 } 917 918 /** 919 * @param waitTime 920 * the waitTime to set 921 */ 922 void setWaitTime(long waitTime) { 923 if (!this.newJob) { 924 this.waitTime = waitTime > 0 ? waitTime : DEFAULT_WAIT; 925 } 926 } 927 928 void pause() { 929 synchronized (mutex) { 930 try { 931 mutex.wait(this.waitTime); 932 } catch (InterruptedException e) { 933 } 934 } 935 } 936 937 void newJob() { 938 this.newJob = true; 939 this.waitTime = DEFAULT_NEW_JOB_WAIT; 940 wakeup(); 941 } 942 943 void clearNewJob() { 944 this.newJob = false; 945 } 946 947 void wakeup() { 948 synchronized (this.mutex) { 949 mutex.notifyAll(); 950 } 951 } 952 } 953}