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}