package de.otto.edison.jobs.service; import de.otto.edison.jobs.definition.JobDefinition; import de.otto.edison.jobs.domain.*; import de.otto.edison.jobs.eventbus.JobEventPublisher; import de.otto.edison.jobs.repository.JobBlockedException; import de.otto.edison.jobs.repository.JobRepository; import de.otto.edison.status.domain.SystemInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.metrics.GaugeService; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import java.time.Clock; import java.time.OffsetDateTime; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.stream.Collectors; import static de.otto.edison.jobs.domain.JobInfo.Builder; import static de.otto.edison.jobs.domain.JobInfo.JobStatus; import static de.otto.edison.jobs.domain.JobInfo.JobStatus.ERROR; import static de.otto.edison.jobs.domain.JobInfo.newJobInfo; import static de.otto.edison.jobs.domain.JobMessage.jobMessage; import static de.otto.edison.jobs.eventbus.JobEventPublisher.newJobEventPublisher; import static de.otto.edison.jobs.service.JobRunner.newJobRunner; import static java.lang.String.format; import static java.lang.System.currentTimeMillis; import static java.time.OffsetDateTime.now; import static java.util.Collections.emptyList; @Service public class JobService { private static final Logger LOG = LoggerFactory.getLogger(JobService.class); @Autowired private ApplicationEventPublisher applicationEventPublisher; @Autowired private JobRepository jobRepository; @Autowired private JobMetaService jobMetaService; @Autowired private ScheduledExecutorService executor; @Autowired private GaugeService gaugeService; @Autowired(required = false) private List<JobRunnable> jobRunnables = emptyList(); @Autowired private UuidProvider uuidProvider; @Autowired private SystemInfo systemInfo; private Clock clock = Clock.systemDefaultZone(); public JobService() { } JobService(final JobRepository jobRepository, final JobMetaService jobMetaService, final List<JobRunnable> jobRunnables, final GaugeService gaugeService, final ScheduledExecutorService executor, final ApplicationEventPublisher applicationEventPublisher, final Clock clock, final SystemInfo systemInfo, final UuidProvider uuidProvider) { this.jobRepository = jobRepository; this.jobMetaService = jobMetaService; this.jobRunnables = jobRunnables; this.gaugeService = gaugeService; this.executor = executor; this.applicationEventPublisher = applicationEventPublisher; this.clock = clock; this.systemInfo = systemInfo; this.uuidProvider = uuidProvider; } @PostConstruct public void postConstruct() { LOG.info("Found {} JobRunnables: {}", +jobRunnables.size(), jobRunnables.stream().map(j -> j.getJobDefinition().jobType()).collect(Collectors.toList())); } /** * Starts a job asynchronously in the background. * * @param jobType the type of the job * @return the URI under which you can retrieve the status about the triggered job instance */ public Optional<String> startAsyncJob(String jobType) { try { final JobRunnable jobRunnable = findJobRunnable(jobType); final JobInfo jobInfo = createJobInfo(jobType); jobMetaService.aquireRunLock(jobInfo.getJobId(), jobInfo.getJobType()); jobRepository.createOrUpdate(jobInfo); return Optional.of(startAsync(metered(jobRunnable), jobInfo.getJobId())); } catch (JobBlockedException e) { LOG.info(e.getMessage()); return Optional.empty(); } } public Optional<JobInfo> findJob(final String id) { return jobRepository.findOne(id); } /** * Find the latest jobs, optionally restricted to jobs of a specified type. * * @param type if provided, the last N jobs of the type are returned, otherwise the last jobs of any type. * @param count the number of jobs to return. * @return a list of JobInfos */ public List<JobInfo> findJobs(final Optional<String> type, final int count) { if (type.isPresent()) { return jobRepository.findLatestBy(type.get(), count); } else { return jobRepository.findLatest(count); } } public List<JobInfo> findJobsDistinct() { return jobRepository.findLatestJobsDistinct(); } public void deleteJobs(final Optional<String> type) { if (type.isPresent()) { jobRepository.findByType(type.get()).forEach((j) -> jobRepository.removeIfStopped(j.getJobId())); } else { jobRepository.findAll().forEach((j) -> jobRepository.removeIfStopped(j.getJobId())); } } public void stopJob(final String jobId) { this.stopJob(jobId, Optional.empty()); } public void killJobsDeadSince(final int seconds) { final OffsetDateTime timeToMarkJobAsStopped = now(clock).minusSeconds(seconds); LOG.info(format("JobCleanup: Looking for jobs older than %s ", timeToMarkJobAsStopped)); final List<JobInfo> deadJobs = jobRepository.findRunningWithoutUpdateSince(timeToMarkJobAsStopped); deadJobs.forEach(deadJob -> killJob(deadJob.getJobId(), deadJob.getJobType())); clearRunLocks(); } /** * Checks all run locks and releases the lock, if the job is stopped. * * TODO: This method should never do something, otherwise the is a bug in the lock handling. * TODO: Check Log files + Remove */ private void clearRunLocks() { jobMetaService.runningJobs().forEach((RunningJob runningJob) -> { final Optional<JobInfo> jobInfoOptional = jobRepository.findOne(runningJob.jobId); if (jobInfoOptional.isPresent() && jobInfoOptional.get().isStopped()) { jobMetaService.releaseRunLock(runningJob.jobType); LOG.error("Clear Lock of Job {}. Job stopped already.", runningJob.jobType); } else if (!jobInfoOptional.isPresent()){ jobMetaService.releaseRunLock(runningJob.jobType); LOG.error("Clear Lock of Job {}. JobID does not exist", runningJob.jobType); } }); } public void killJob(final String jobId, final String jobType) { stopJob(jobId, Optional.of(JobStatus.DEAD)); jobRepository.appendMessage(jobId, jobMessage(Level.WARNING, "Job didn't receive updates for a while, considering it dead", now(clock))); } private void stopJob(final String jobId, final Optional<JobStatus> status) { jobRepository.findOne(jobId).ifPresent((JobInfo jobInfo) -> { jobMetaService.releaseRunLock(jobInfo.getJobType()); final OffsetDateTime now = now(clock); final Builder builder = jobInfo.copy() .setStopped(now) .setLastUpdated(now); status.ifPresent(builder::setStatus); jobRepository.createOrUpdate(builder.build()); }); } public void appendMessage(String jobId, JobMessage jobMessage) { // TODO: Refactor JobRepository so only a single update is required jobRepository.appendMessage(jobId, jobMessage); if (jobMessage.getLevel() == Level.ERROR) { jobRepository.findOne(jobId).ifPresent(jobInfo -> { jobRepository.createOrUpdate( jobInfo.copy() .setStatus(ERROR) .setLastUpdated(now(clock)) .build()); }); } } public void keepAlive(String jobId) { jobRepository.setLastUpdate(jobId, now(clock)); } public void markSkipped(String jobId) { // TODO: Refactor JobRepository so only a single update is required OffsetDateTime currentTimestamp = now(clock); jobRepository.appendMessage(jobId, jobMessage(Level.INFO, "Skipped job ..", currentTimestamp)); jobRepository.setLastUpdate(jobId, currentTimestamp); jobRepository.setJobStatus(jobId, JobStatus.SKIPPED); } public void markRestarted(String jobId) { // TODO: Refactor JobRepository so only a single update is required OffsetDateTime currentTimestamp = now(clock); jobRepository.appendMessage(jobId, jobMessage(Level.WARNING, "Restarting job ..", currentTimestamp)); jobRepository.setLastUpdate(jobId, currentTimestamp); jobRepository.setJobStatus(jobId, JobStatus.OK); } private JobInfo createJobInfo(String jobType) { return newJobInfo(uuidProvider.getUuid(), jobType, clock, systemInfo.getHostname()); } private JobRunnable findJobRunnable(String jobType) { final Optional<JobRunnable> optionalRunnable = jobRunnables.stream().filter(r -> r.getJobDefinition().jobType().equalsIgnoreCase(jobType)).findFirst(); return optionalRunnable.orElseThrow(() -> new IllegalArgumentException("No JobRunnable for " + jobType)); } private String startAsync(final JobRunnable jobRunnable, final String jobId) { final JobRunner jobRunner = createJobRunner(jobRunnable, jobId); executor.execute(() -> jobRunner.start(jobRunnable)); return jobId; } private JobRunner createJobRunner(JobRunnable jobRunnable, String jobId) { final String jobType = jobRunnable.getJobDefinition().jobType(); return newJobRunner( jobId, jobType, executor, newJobEventPublisher(applicationEventPublisher, jobRunnable, jobId) ); } private JobRunnable metered(final JobRunnable delegate) { return new JobRunnable() { @Override public JobDefinition getJobDefinition() { return delegate.getJobDefinition(); } @Override public void execute(final JobEventPublisher jobEventPublisher) { long ts = currentTimeMillis(); delegate.execute(jobEventPublisher); gaugeService.submit(gaugeName(), (currentTimeMillis() - ts) / 1000L); } private String gaugeName() { return "gauge.jobs.runtime." + delegate.getJobDefinition().jobType().toLowerCase(); } }; } }