package com.anjlab.ping.entities; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import org.datanucleus.jpa.annotations.Extension; import javax.persistence.Basic; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.PrePersist; import javax.persistence.PreUpdate; import javax.persistence.Transient; import org.apache.tapestry5.beaneditor.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.anjlab.gae.SerializableEstimations; import com.anjlab.ping.services.Application; import com.anjlab.ping.services.Utils; import com.google.appengine.api.datastore.Blob; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.Text; @Entity public class Job implements Serializable, SerializableEstimations { public enum HealthStatus { OK, Warning, Error, Unknown } /** * */ private static final long serialVersionUID = -1077399963209971165L; private static final Logger logger = LoggerFactory.getLogger(Job.class); @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Key key; // URL to ping @Column(nullable=false) @Validate("required,regexp=(http://|https://).+") private String pingURL; // Validating regexp @Column(nullable=true) @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private String validatingRegexp; // Cron string to select jobs against a cron action @Column(nullable=false) @Validate("required") private String cronString; // Email to send reports to @Validate("email") @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private String reportEmail; @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private Date lastPingTimestamp; @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private int lastPingResult; @Basic private Text lastPingDetails; @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private boolean usesValidatingRegexp; @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private boolean usesValidatingHttpCode; @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private Integer validatingHttpCode; @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private String responseEncoding; @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private String title; // Number of times this job stays in failed or okay status @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private Integer statusCounter; @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private Integer previousStatusCounter; @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private Integer totalStatusCounter; @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private Integer totalSuccessStatusCounter; @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private Boolean receiveNotifications; @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private Boolean receiveBackups; @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private Date lastBackupTimestamp; // Since 13.05.2010 @Basic private Blob packedJobResults; @Transient // Add transient keyword to not serialize this field // (cache throws "Policy prevented put operation" exception for big objects) private transient List<JobResult> jobResults; @Transient private boolean updatingJobResults; @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private Date createdAt; // Since 14.02.2012 @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private String suspendReason; // Intentionally left indexed private Date suspendedAt; @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private String suspendedBy; @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private Date modifiedAt; @Extension(vendorName="datanucleus", key="gae.unindexed", value="true") private String modifiedBy; private String scheduleName; public static final int PING_RESULT_NOT_AVAILABLE = 1; public static final int PING_RESULT_OK = 2; public static final int PING_RESULT_CONNECTIVITY_PROBLEM = 4; public static final int PING_RESULT_HTTP_ERROR = 8; public static final int PING_RESULT_REGEXP_VALIDATION_FAILED = 16; public Job copy() { Job copy = new Job(); copy.createdAt = createdAt; copy.updatingJobResults = updatingJobResults; copy.jobResults = jobResults; copy.packedJobResults = packedJobResults; copy.lastBackupTimestamp = lastBackupTimestamp; copy.receiveBackups = receiveBackups; copy.receiveNotifications = receiveNotifications; copy.totalSuccessStatusCounter = totalSuccessStatusCounter; copy.totalStatusCounter = totalStatusCounter; copy.previousStatusCounter = previousStatusCounter; copy.statusCounter = statusCounter; copy.title = title; copy.responseEncoding = responseEncoding; copy.validatingHttpCode = validatingHttpCode; copy.usesValidatingHttpCode = usesValidatingHttpCode; copy.usesValidatingRegexp = usesValidatingRegexp; copy.lastPingDetails = lastPingDetails; copy.lastPingResult = lastPingResult; copy.lastPingTimestamp = lastPingTimestamp; copy.reportEmail = reportEmail; copy.cronString = cronString; copy.validatingRegexp = validatingRegexp; copy.pingURL = pingURL; copy.suspendedAt = suspendedAt; copy.suspendedBy = suspendedBy; copy.suspendReason = suspendReason; copy.modifiedAt = modifiedAt; copy.modifiedBy = modifiedBy; copy.scheduleName = scheduleName; return copy; } public Job() { lastPingResult = PING_RESULT_NOT_AVAILABLE; lastBackupTimestamp = new Date(); } public boolean isLastPingFailed() { return ! (containsResult(lastPingResult, PING_RESULT_NOT_AVAILABLE) || containsResult(lastPingResult, PING_RESULT_OK)); } public static boolean containsResult(int result, int resultCode) { return (result & resultCode) == resultCode; } public String getValidationSummary() { StringBuilder sb = new StringBuilder(); if (usesValidatingHttpCode) { sb.append("HTTP Code"); } if (usesValidatingRegexp) { if (sb.length() > 0) { sb.append(", "); } sb.append("Regexp"); } if (sb.length() == 0) { sb.append("None"); } return sb.toString(); } public String getPingURL() { return pingURL; } public void setPingURL(String pingURL) { this.pingURL = pingURL; } public String getValidatingRegexp() { return validatingRegexp; } public void setValidatingRegexp(String validatingRegexp) { this.validatingRegexp = validatingRegexp; } public String getCronString() { return cronString; } public void setCronString(String cronString) { this.cronString = cronString; } public String getReportEmail() { return reportEmail; } public void setReportEmail(String reportEmail) { this.reportEmail = reportEmail; } public Key getKey() { return key; } public Date getLastPingTimestamp() { return lastPingTimestamp; } public void setLastPingTimestamp(Date lastPingTimestamp) { this.lastPingTimestamp = lastPingTimestamp; } public Date getLastBackupTimestamp() { return lastBackupTimestamp; } public void setLastBackupTimestamp(Date lastBackupTimestamp) { this.lastBackupTimestamp = lastBackupTimestamp; } public int getLastPingResult() { return lastPingResult; } public void setLastPingResult(int lastPingResult) { this.lastPingResult = lastPingResult; } public String getLastPingDetails() { return lastPingDetails == null ? null : lastPingDetails.getValue(); } public void setLastPingDetails(String lastPingDetails) { if (lastPingDetails != null && lastPingDetails.length() > 1024 * 100) { // Persist only 100 KB of ping details lastPingDetails = lastPingDetails.substring(0, 1024 * 100); } if (lastPingDetails == null) { lastPingDetails = ""; } this.lastPingDetails = new Text(lastPingDetails); } public boolean isUsesValidatingRegexp() { return usesValidatingRegexp; } public void setUsesValidatingRegexp(boolean usesValidatingRegexp) { this.usesValidatingRegexp = usesValidatingRegexp; } public boolean isUsesValidatingHttpCode() { return usesValidatingHttpCode; } public void setUsesValidatingHttpCode(boolean usesValidatingHttpCode) { this.usesValidatingHttpCode = usesValidatingHttpCode; } public Integer getValidatingHttpCode() { return validatingHttpCode; } public void setValidatingHttpCode(Integer validatingHttpCode) { this.validatingHttpCode = validatingHttpCode; } public String getScheduledBy() { return scheduleName; } public String getResponseEncoding() { return responseEncoding; } public void setResponseEncoding(String responseEncoding) { this.responseEncoding = responseEncoding; } public String getShortenURL() { if (pingURL == null) { return null; } return pingURL.length() > 47 ? pingURL.substring(0, 46) + "..." : pingURL; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getTitleFriendly() { if (Utils.isNullOrEmpty(title)) { return getShortenURL(); } return title; } public int getStatusCounter() { return statusCounter == null ? 0 : statusCounter; } public String getStatusCounterFriendly() { return formatCounter(getStatusCounter()); } public String getStatusCounterFriendlyShort() { return formatCounterShort(getStatusCounter()); } public int getUpDownTimeInMinutes() { return Utils.getTimeInMinutes(getStatusCounter(), cronString); } private String formatCounter(int counter) { return formatCounterShort(counter) + " (" + counter + ")"; } private String formatCounterShort(int counter) { return Utils.formatMinutesToWordsUpToMinutes(Utils.getTimeInMinutes(counter, cronString)); } private void setStatusCounter(int statusCounter) { this.statusCounter = statusCounter; } public int getPreviousStatusCounter() { return previousStatusCounter == null ? 0 : previousStatusCounter; } public String getPreviousStatusCounterFriendly() { return formatCounter(getPreviousStatusCounter()); } private void setPreviousStatusCounter(int previousStatusCounter) { this.previousStatusCounter = previousStatusCounter; } public boolean isGoogleIOException() { return isLastPingFailed() && getLastPingResult() == PING_RESULT_CONNECTIVITY_PROBLEM && getLastPingDetails() != null && getLastPingDetails().contains("google") && (getLastPingDetails().contains("java.io.IOException: Timeout") || getLastPingDetails().contains("java.net.SocketTimeoutException: Timeout") || getLastPingDetails().contains("java.io.IOException: Could not fetch URL") || getLastPingDetails().contains("DeadlineExceededException")); } public int getTotalStatusCounter() { return totalStatusCounter == null ? getPreviousStatusCounter() + getStatusCounter() : totalStatusCounter; } public String getTotalStatusCounterFriendly() { return formatCounter(getTotalStatusCounter()); } private void incrementTotalStatusCounter() { int correction = 0; if (totalStatusCounter == null) { correction = -1; } this.totalStatusCounter = getTotalStatusCounter() + 1 + correction; if (! isLastPingFailed()) { incrementTotalSuccessStatusCounter(); } } public int getTotalSuccessStatusCounter() { return totalSuccessStatusCounter == null ? (isLastPingFailed() ? getPreviousStatusCounter() : getStatusCounter()) : totalSuccessStatusCounter; } public String getTotalSuccessStatusCounterFriendly() { return formatCounter(getTotalSuccessStatusCounter()); } private void incrementTotalSuccessStatusCounter() { int correction = 0; if (totalSuccessStatusCounter == null) { correction = -1; } this.totalSuccessStatusCounter = getTotalSuccessStatusCounter() + 1 + correction; } public double getTotalAvailabilityPercent() { return Utils.calculatePercent(getTotalStatusCounter(), getTotalSuccessStatusCounter()); } public String getTotalAvailabilityPercentFriendly() { return Utils.formatPercent(getTotalAvailabilityPercent()); } public double getRecentAvailabilityPercent() { int recentCount = Application.DEFAULT_NUMBER_OF_JOB_RESULTS; List<JobResult> results = getRecentJobResults(recentCount); return Utils.calculateAvailabilityPercent(results); } public String getRecentAvailabilityPercentFriendly() { return Utils.formatPercentShort(getRecentAvailabilityPercent()); } public void resetStatusCounter() { setPreviousStatusCounter(getStatusCounter()); setStatusCounter(0); incrementStatusCounter(); } public void incrementStatusCounter() { setStatusCounter(getStatusCounter() + 1); incrementTotalStatusCounter(); } public void setReceiveNotifications(boolean receiveNotifications) { this.receiveNotifications = receiveNotifications; } public boolean isReceiveNotifications() { return receiveNotifications == null || // Receive notifications by default receiveNotifications.booleanValue(); } public void setReceiveBackups(boolean receiveBackups) { this.receiveBackups = receiveBackups; } public boolean isReceiveBackups() { return receiveBackups == null || // Receive backups by default receiveBackups.booleanValue(); } @Override public int hashCode() { return getKey() == null ? super.hashCode() : getKey().hashCode(); } @Override public boolean equals(Object obj) { if (!(obj instanceof Job)) { return false; } Job job = (Job) obj; return getKey() == null ? super.equals(obj) : getKey().equals(job.getKey()); } public List<JobResult> getRecentJobResults2(int numberOfResults) { List<JobResult> results = getRecentJobResults(numberOfResults); if (results.size() > 0) { Date dateFrom = results.get(0).getTimestamp(); Date dateTo = results.get(results.size() - 1).getTimestamp(); // Truncate results to one month back Date monthBack = monthBack(dateTo); if (dateTo.getTime() - dateFrom.getTime() > dateTo.getTime() - monthBack.getTime()) { JobResult jobResult = new JobResult(); jobResult.setTimestamp(monthBack); int index = Collections.binarySearch(results, jobResult, new Comparator<JobResult>() { @Override public int compare(JobResult a, JobResult b) { int dateDiffMillis = (int) (a.getTimestamp().getTime() - b.getTimestamp().getTime()); int oneDayMillis = 1000 * 60 * 60 * 24; return Math.abs(dateDiffMillis) < oneDayMillis ? 0 : dateDiffMillis; } }); if (index > 0) { results = results.subList(index, results.size()); } } } return results; } /** * If client changes returned results he is responsible * for those changes to be persisted back to {@value #packedJobResults}, * e.g., by calling {@link #endUpdateJobResults()}. * * @return At most <code>numberOfResults</code> of recently added job results. * If <code>numberOfResults == 0</code> returns all results. * @since 13.05.2010 */ public List<JobResult> getRecentJobResults(int numberOfResults) { readJobResults(); return numberOfResults == 0 ? jobResults : (jobResults.size() == 0 ? jobResults : jobResults.subList( jobResults.size() > numberOfResults ? jobResults.size() - numberOfResults : 0, jobResults.size())); } private Date monthBack(Date from) { Calendar instance = Calendar.getInstance(); instance.setTime(from); instance.add(Calendar.MONTH, -1); return instance.getTime(); } public int getResultsCount() { readJobResults(); return jobResults.size(); } @SuppressWarnings("unchecked") private void readJobResults() { if (jobResults == null) { if (packedJobResults == null || packedJobResults.getBytes().length == 0) { jobResults = new ArrayList<JobResult>(); } else { ObjectInputStream ois = null; try { ois = new ObjectInputStream(new ByteArrayInputStream(packedJobResults.getBytes())); jobResults = (List<JobResult>) ois.readObject(); } catch (Exception e) { logger.error("Error unpacking job results", e); jobResults = new ArrayList<JobResult>(); } finally { if (ois != null) { try { ois.close(); } catch (IOException e) { logger.error("Error closing ois", e); } } } } } } /** * Call this method before batch adding job results * * @since 13.05.2010 */ public void beginUpdateJobResults() { updatingJobResults = true; } /** * Call this method to flush batch added job results * * @since 13.05.2010 */ public void endUpdateJobResults() { updatingJobResults = false; packJobResults(); } /** * * @param jobResult * * @since 13.05.2010 */ public void addJobResult(JobResult jobResult) { readJobResults(); jobResults.add(jobResult); if (!updatingJobResults) { packJobResults(); } } public void addJobResult(int index, JobResult result) { readJobResults(); jobResults.add(index, result); if (!updatingJobResults) { packJobResults(); } } public List<JobResult> removeJobResultsExceptRecent(int numberOfResultsToLeave) { readJobResults(); List<JobResult> results = new ArrayList<JobResult>(); while (jobResults.size() > numberOfResultsToLeave) { results.add(jobResults.get(0)); jobResults.remove(0); } if (results.size() > 0) { packJobResults(); } return results; } /** * * @throws IOException * @since 13.05.2010 */ @PrePersist @PreUpdate void packJobResults() { if (jobResults != null) { ByteArrayOutputStream baos = new ByteArrayOutputStream(jobResults.size() * 14 + 365); ObjectOutputStream oos = null; try { oos = new ObjectOutputStream(baos); oos.writeObject(jobResults); } catch (IOException e) { logger.error("Error packing job results", e); } finally { if (oos != null) { try { oos.close(); } catch (IOException e) { logger.error("Error closing oos", e); } } } packedJobResults = new Blob(baos.toByteArray()); } } int getPackedJobResultsLength() { return packedJobResults == null ? 0 : packedJobResults.getBytes().length; } public Date getCreatedAt() { if (createdAt == null) { Calendar calendar = Calendar.getInstance(); int ageInMinutes = Utils.getTimeInMinutes(getTotalStatusCounter(), cronString); calendar.add(Calendar.MINUTE, - ageInMinutes); createdAt = calendar.getTime(); } return createdAt; } public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; } @Override public String toString() { return (key == null ? "null" : key.toString()) + " " + cronString + " " + pingURL; } public String getLastPingSummary() { StringBuilder sb = new StringBuilder(); if (getLastPingTimestamp() != null) { String timeAgo = Utils.getTimeAgoUpToMinutes(getLastPingTimestamp()); buildPingResultSummary(getLastPingResult(), sb); sb.append(" / "); sb.append(timeAgo); } else { sb.append("N/A"); } return sb.toString(); } public static void buildPingResultSummary(int pingResult, StringBuilder sb) { buildPingResultSummary(pingResult, sb, null); } public static void buildPingResultSummary(int pingResult, StringBuilder sb, JobResult jobResult) { checkResult(pingResult, sb, PING_RESULT_NOT_AVAILABLE, "N/A"); checkResult(pingResult, sb, PING_RESULT_OK, "Okay"); checkResult(pingResult, sb, PING_RESULT_HTTP_ERROR, "HTTP failed" + (jobResult != null ? " (" + jobResult.getHTTPResponseCode() + ")" : "")); checkResult(pingResult, sb, PING_RESULT_CONNECTIVITY_PROBLEM, "Failed connecting"); checkResult(pingResult, sb, PING_RESULT_REGEXP_VALIDATION_FAILED, "Regexp failed"); } private static void checkResult(int pingResult, StringBuilder sb, int resultCode, String message) { if (containsResult(pingResult, resultCode)) { if (sb.length() > 0) { sb.append(", "); } sb.append(message); } } public static String buildPingResultSummary(int resultCode) { StringBuilder sb = new StringBuilder(); buildPingResultSummary(resultCode, sb); return sb.toString(); } public HealthStatus getHealthStatus() { if (this.isGoogleIOException()) { return HealthStatus.Warning; } if (this.isLastPingFailed()) { return HealthStatus.Error; } if (this.getTotalStatusCounter() == 0) { return HealthStatus.Unknown; } if (isLastPingWasTooLongAgo()) { return HealthStatus.Warning; } return HealthStatus.OK; } public boolean isLastPingWasTooLongAgo() { if (this.getLastPingTimestamp() == null) { return true; } final int tooLongDelayLimit = 1000 * 60 * 60 * (24 + 1 /* hours */); long lastPingDelay = System.currentTimeMillis() - this.getLastPingTimestamp().getTime(); return lastPingDelay >= tooLongDelayLimit; } public boolean isSuspended() { return this.suspendedAt != null; } public void suspend(String suspendReason, String username) { this.suspendedAt = new Date(); this.suspendReason = suspendReason; this.suspendedBy = username; } public void resume() { this.suspendedAt = null; this.suspendReason = null; this.suspendedBy = null; } public String getSuspendReason() { return suspendReason; } public String getSuspendedBy() { return suspendedBy; } public Date getSuspendedAt() { return suspendedAt; } public void fireModified(String modifiedBy) { this.modifiedAt = new Date(); this.modifiedBy = modifiedBy; } public void clearModified() { this.modifiedAt = null; this.modifiedBy = null; } public Date getModifiedAt() { return modifiedAt; } public String getModifiedBy() { return modifiedBy; } public void setScheduleName(String scheduleName) { this.scheduleName = scheduleName; } public String getScheduleName() { return scheduleName; } @Override public int getEstimatedSerializedSize() { int minimumSerializedSize = 1000; return packedJobResults == null ? minimumSerializedSize : minimumSerializedSize + packedJobResults.getBytes().length; } public JobResult getFirstResult() { readJobResults(); return jobResults.size() > 0 ? jobResults.get(0) : null; } public JobResult getLastResult() { readJobResults(); return jobResults.size() > 0 ? jobResults.get(jobResults.size() - 1) : null; } }