All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ovea.tajin.framework.scheduling.AsyncJobScheduler.groovy Maven / Gradle / Ivy

There is a newer version: 3.9
Show newest version
/**
 * Copyright (C) 2011 Ovea 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.ovea.tajin.framework.scheduling

import com.google.common.cache.LoadingCache
import com.google.common.util.concurrent.ThreadFactoryBuilder
import com.ovea.tajin.framework.jmx.JmxSelfNaming
import com.ovea.tajin.framework.jmx.annotation.JmxBean
import com.ovea.tajin.framework.jmx.annotation.JmxProperty
import com.ovea.tajin.framework.util.PropertySettings
import org.slf4j.Logger
import org.slf4j.LoggerFactory

import javax.annotation.PostConstruct
import javax.annotation.PreDestroy
import javax.inject.Inject
import javax.management.MalformedObjectNameException
import javax.management.ObjectName
import java.util.concurrent.*
import java.util.concurrent.atomic.AtomicLong

/**
 * @author Mathieu Carbou ([email protected])
 * @date 2013-06-06
 */
@javax.inject.Singleton
@JmxBean
class AsyncJobScheduler implements JobScheduler, JmxSelfNaming {

    private static final Logger LOGGER = LoggerFactory.getLogger(AsyncJobScheduler)
    private static final long MINS_5 = TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES)
    private static final AtomicLong INSTANCES = new AtomicLong(-1)

    @Inject
    LoadingCache executors

    @Inject
    JobRepository repository

    @Inject
    PropertySettings settings

    JobScheduler.OnError onError = JobScheduler.OnError.LOG

    private ScheduledExecutorService service
    private int maxRetry = -1

    private final ConcurrentHashMap> scheduledJobs = new ConcurrentHashMap<>()
    private final AtomicLong nRunning = new AtomicLong()
    private final AtomicLong nRan = new AtomicLong()
    private final AtomicLong nFailed = new AtomicLong()

    AsyncJobScheduler() {
        INSTANCES.incrementAndGet()
    }

    @JmxProperty
    long getScheduledCount() { scheduledJobs.size() }

    @JmxProperty
    long getRunningCount() { nRunning.get() }

    @JmxProperty
    long getTotalExecutionCount() { nRan.get() }

    @JmxProperty
    long getFailedCount() { nFailed.get() }

    @JmxProperty
    Collection getScheduledJobs() {
        long now = System.currentTimeMillis()
        scheduledJobs.collect { k, v -> "${k.id} ${k.name} in ${Math.max(0, k.start.time - now) / 1000}s" as String }
    }

    @Override
    ObjectName getObjectName() throws MalformedObjectNameException {
        return new ObjectName("com.ovea.tajin:type=${getClass().simpleName},name=${INSTANCES.get()}")
    }

    @PostConstruct
    void init() {
        maxRetry = settings.getInt('scheduling.maxRetry', -1)
        service = new ScheduledThreadPoolExecutor(
            settings.getInt('scheduling.pool.size', Runtime.runtime.availableProcessors() * 2),
            new ThreadFactoryBuilder()
                .setDaemon(false)
                .setNameFormat("${AsyncJobScheduler.simpleName}-thread-%d")
                .setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
                @Override
                void uncaughtException(Thread t, Throwable e) {
                    LOGGER.error("UncaughtException in Job Scheduler: ${e.message}", e)
                }
            }).build()
        )
        List jobs = repository.listPendingJobs()
        // filter jobs that can be retried
        if (maxRetry > -1) {
            jobs = jobs.findAll { it.retry <= maxRetry }
        }
        jobs.each { doSchedule(it) }
    }

    @PreDestroy
    void close() {
        while (scheduledJobs) {
            scheduledJobs.keySet().each { scheduledJobs.remove(it)?.cancel(false) }
        }
        service.shutdown()
        try {
            service.awaitTermination(10, TimeUnit.SECONDS)
        } catch (ignored) {
        }
    }

    @Override
    Job schedule(String jobName, Map data) { schedule(jobName, new Date(), data) }

    @Override
    void cancel(Collection jobIds) {
        if (jobIds) {
            def entries = scheduledJobs.find { k, v -> k.id in jobIds }
            def now = new Date()
            entries.each {
                it.value.cancel(false)
                it.key.updatedDate = now
                scheduledJobs.remove(it.key)
            }
            repository.delete(entries.collect { it.key })
        }
    }

    @Override
    Job schedule(String jobName, Date time, Map data) {
        if (!jobName) throw new IllegalArgumentException('Missing jobName')
        if (!time) throw new IllegalArgumentException('Missing time')
        // will fail if jobName not found
        executors.get(jobName)
        // create and process job
        Job job = new Job(
            name: jobName,
            start: time,
            data: data ? data.deepClone() : [:]
        )
        // save the job then after save, schedule it
        save job, { doSchedule(job) }
        return job
    }

    private void doSchedule(Job job) {
        if (!service.shutdown && !service.terminated) {
            long diff = Math.max(0, job.start.time - System.currentTimeMillis())
            LOGGER.trace("Scheduling: ${job} in ${diff / 1000}s")
            ScheduledFuture future = service.schedule(new JobRunner(job), diff, TimeUnit.MILLISECONDS)
            scheduledJobs.put(job, future)
        } else {
            throw new IllegalStateException('Job Scheduler is closing or closed and cannot accept new job. Job ' + job + ' will be executed at next startup.')
        }
    }

    private void save(Job job, Closure then = Closure.IDENTITY) {
        job.updatedDate = new Date()
        repository.save(job)
        then()
    }

    private class JobRunner implements Runnable {
        final Job job

        JobRunner(Job job) {
            super()
            this.job = job
        }

        @Override
        void run() {
            try {
                nRunning.incrementAndGet()
                executors.get(job.name).execute(job.data)
                scheduledJobs.remove(job)
                job.end = new Date()
                save job
            } catch (e) {
                scheduledJobs.remove(job)
                nFailed.incrementAndGet()
                job.start = new Date(System.currentTimeMillis() + MINS_5)
                job.retry++
                save job, {
                    doSchedule(job)
                    onError.onError(job, e)
                }
            } finally {
                nRunning.decrementAndGet()
                nRan.incrementAndGet()
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy