package de.otto.edison.mongo.jobs; import static java.time.Clock.systemDefaultZone; import static java.util.Collections.emptyList; import static java.util.Collections.singletonMap; import static java.util.Date.from; import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.toList; import static com.mongodb.ReadPreference.primaryPreferred; import static com.mongodb.client.model.Filters.eq; import static com.mongodb.client.model.Updates.push; import static com.mongodb.client.model.Updates.set; import static de.otto.edison.jobs.domain.JobInfo.newJobInfo; import static de.otto.edison.jobs.domain.JobMessage.jobMessage; import java.time.Clock; import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import org.bson.Document; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mongodb.BasicDBObject; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; import com.mongodb.client.model.Filters; import de.otto.edison.jobs.domain.JobInfo; import de.otto.edison.jobs.domain.JobInfo.JobStatus; import de.otto.edison.jobs.domain.JobMessage; import de.otto.edison.jobs.domain.Level; import de.otto.edison.jobs.repository.JobRepository; import de.otto.edison.mongo.AbstractMongoRepository; public class MongoJobRepository extends AbstractMongoRepository<String, JobInfo> implements JobRepository { private static final Logger LOG = LoggerFactory.getLogger(MongoJobRepository.class); private static final int DESCENDING = -1; private static final String NO_LOG_MESSAGE_FOUND = "No log message found"; public static final String ID = "_id"; private final MongoCollection<Document> jobInfoCollection; private final Clock clock; public MongoJobRepository(final MongoDatabase mongoDatabase, final String jobInfoCollectionName) { this.jobInfoCollection = mongoDatabase.getCollection(jobInfoCollectionName).withReadPreference(primaryPreferred()); this.clock = systemDefaultZone(); } @Override public JobStatus findStatus(final String jobId) { return JobStatus.valueOf(collection() .find(eq(ID, jobId)) .projection(new Document(JobStructure.STATUS.key(), true)) .first().getString(JobStructure.STATUS.key())); } @Override public void removeIfStopped(final String id) { findOne(id).ifPresent(jobInfo -> { if (jobInfo.isStopped()) { collection().deleteOne(eq(ID, id)); } }); } @Override public void appendMessage(final String jobId, final JobMessage jobMessage) { collection().updateOne(eq(ID, jobId), push(JobStructure.MESSAGES.key(), encodeJobMessage(jobMessage))); } @Override public void setJobStatus(final String jobId, final JobStatus jobStatus) { collection().updateOne(eq(ID, jobId), set(JobStructure.STATUS.key(), jobStatus.name())); } @Override public void setLastUpdate(final String jobId, final OffsetDateTime lastUpdate) { collection().updateOne(eq(ID, jobId), set(JobStructure.LAST_UPDATED.key(), DateTimeConverters.toDate(lastUpdate))); } @Override public List<JobInfo> findLatest(final int maxCount) { return collection() .find() .sort(orderByStarted(DESCENDING)) .limit(maxCount) .map(this::decode) .into(new ArrayList<>()); } @Override public List<JobInfo> findLatestJobsDistinct() { final List<String> allJobIds = findAllJobIdsDistinct(); return collection() .find(Filters.in(ID, allJobIds)) .map(this::decode) .into(new ArrayList<>()); } public List<String> findAllJobIdsDistinct() { return collection() .aggregate(Arrays.asList( new Document("$sort", new Document("started", -1)), new Document("$group", new HashMap<String, Object>() {{ put("_id", "$type"); put("latestJobId", new Document("$first", "$_id")); }}))) .map(doc -> doc.getString("latestJobId")) .into(new ArrayList<>()).stream() .filter(Objects::nonNull) .collect(toList()); } @Override public List<JobInfo> findLatestBy(final String type, final int maxCount) { return collection() .find(byType(type)) .sort(orderByStarted(DESCENDING)) .limit(maxCount) .map(this::decode) .into(new ArrayList<>()); } @Override public List<JobInfo> findByType(final String type) { return collection() .find(byType(type)) .sort(orderByStarted(DESCENDING)) .map(this::decode) .into(new ArrayList<>()); } @Override public List<JobInfo> findRunningWithoutUpdateSince(final OffsetDateTime timeOffset) { return collection() .find(new Document() .append(JobStructure.STOPPED.key(), singletonMap("$exists", false)) .append(JobStructure.LAST_UPDATED.key(), singletonMap("$lt", from(timeOffset.toInstant())))) .map(this::decode) .into(new ArrayList<>()); } @Override protected final Document encode(final JobInfo job) { final Document document = new Document() .append(JobStructure.ID.key(), job.getJobId()) .append(JobStructure.JOB_TYPE.key(), job.getJobType()) .append(JobStructure.STARTED.key(), DateTimeConverters.toDate(job.getStarted())) .append(JobStructure.LAST_UPDATED.key(), DateTimeConverters.toDate(job.getLastUpdated())) .append(JobStructure.MESSAGES.key(), job.getMessages().stream() .map(MongoJobRepository::encodeJobMessage) .collect(toList())) .append(JobStructure.STATUS.key(), job.getStatus().name()) .append(JobStructure.HOSTNAME.key(), job.getHostname()); if (job.isStopped()) { document.append(JobStructure.STOPPED.key(), DateTimeConverters.toDate(job.getStopped().get())); } return document; } private static Document encodeJobMessage(final JobMessage jm) { return new Document() {{ put(JobStructure.MSG_LEVEL.key(), jm.getLevel().name()); put(JobStructure.MSG_TS.key(), DateTimeConverters.toDate(jm.getTimestamp())); put(JobStructure.MSG_TEXT.key(), jm.getMessage()); }}; } @Override protected final JobInfo decode(final Document document) { return newJobInfo( document.getString(JobStructure.ID.key()), document.getString(JobStructure.JOB_TYPE.key()), DateTimeConverters.toOffsetDateTime(document.getDate(JobStructure.STARTED.key())), DateTimeConverters.toOffsetDateTime(document.getDate(JobStructure.LAST_UPDATED.key())), ofNullable(DateTimeConverters.toOffsetDateTime(document.getDate(JobStructure.STOPPED.key()))), JobStatus.valueOf(document.getString(JobStructure.STATUS.key())), getMessagesFrom(document), clock, document.getString(JobStructure.HOSTNAME.key())); } @SuppressWarnings("unchecked") private List<JobMessage> getMessagesFrom(final Document document) { final List<Document> messages = (List<Document>) document.get(JobStructure.MESSAGES.key()); if (messages != null) { return messages.stream() .map(this::toJobMessage) .collect(toList()); } else { return emptyList(); } } private JobMessage toJobMessage(final Document document) { return jobMessage( Level.valueOf(document.get(JobStructure.MSG_LEVEL.key()).toString()), getMessage(document), DateTimeConverters.toOffsetDateTime(document.getDate(JobStructure.MSG_TS.key())) ); } @Override protected final String keyOf(final JobInfo value) { return value.getJobId(); } @Override protected final MongoCollection<Document> collection() { return jobInfoCollection; } @Override protected final void ensureIndexes() { collection().createIndex(new BasicDBObject(JobStructure.JOB_TYPE.key(), 1)); collection().createIndex(new BasicDBObject(JobStructure.STARTED.key(), 1)); } private String getMessage(final Document document) { return document.get(JobStructure.MSG_TEXT.key()) == null ? NO_LOG_MESSAGE_FOUND : document.get(JobStructure.MSG_TEXT.key()).toString(); } private Document byType(final String type) { return new Document(JobStructure.JOB_TYPE.key(), type); } private Document byTypeAndStatus(final String type, final JobStatus status) { return new Document(JobStructure.JOB_TYPE.key(), type).append(JobStructure.STATUS.key(), status.name()); } private Document orderByStarted(final int order) { return new Document(JobStructure.STARTED.key(), order); } @Override public List<JobInfo> findAllJobInfoWithoutMessages() { return collection() .find() .projection(new Document(getJobInfoWithoutMessagesProjection())) .map(this::decode) .into(new ArrayList<>()); } private Map<String, Object> getJobInfoWithoutMessagesProjection() { final Map<String, Object> projection = new HashMap<>(); projection.put(JobStructure.ID.key(), true); projection.put(JobStructure.JOB_TYPE.key(), true); projection.put(JobStructure.STARTED.key(), true); projection.put(JobStructure.LAST_UPDATED.key(), true); projection.put(JobStructure.STOPPED.key(), true); projection.put(JobStructure.STATUS.key(), true); projection.put(JobStructure.HOSTNAME.key(), true); return projection; } }