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