1 /* 2 * Copyright (c) 2017 Google Inc. All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you 5 * may not use this file except in compliance with the License. You may 6 * obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 13 * implied. See the License for the specific language governing 14 * permissions and limitations under the License. 15 */ 16 17 package com.android.vts.job; 18 19 import com.android.vts.entity.TestEntity; 20 import com.android.vts.entity.TestRunEntity; 21 import com.android.vts.entity.TestStatusEntity; 22 import com.android.vts.util.EmailHelper; 23 import com.android.vts.util.FilterUtil; 24 import com.android.vts.util.TaskQueueHelper; 25 import com.android.vts.util.TimeUtil; 26 import com.google.appengine.api.datastore.DatastoreService; 27 import com.google.appengine.api.datastore.DatastoreServiceFactory; 28 import com.google.appengine.api.datastore.Entity; 29 import com.google.appengine.api.datastore.EntityNotFoundException; 30 import com.google.appengine.api.datastore.FetchOptions; 31 import com.google.appengine.api.datastore.Key; 32 import com.google.appengine.api.datastore.KeyFactory; 33 import com.google.appengine.api.datastore.Query; 34 import com.google.appengine.api.datastore.Query.Filter; 35 import com.google.appengine.api.datastore.Query.SortDirection; 36 import com.google.appengine.api.taskqueue.Queue; 37 import com.google.appengine.api.taskqueue.QueueFactory; 38 import com.google.appengine.api.taskqueue.TaskOptions; 39 import java.io.IOException; 40 import java.io.UnsupportedEncodingException; 41 import java.util.ArrayList; 42 import java.util.List; 43 import java.util.concurrent.TimeUnit; 44 import java.util.logging.Level; 45 import java.util.logging.Logger; 46 import javax.mail.Message; 47 import javax.mail.MessagingException; 48 import javax.servlet.http.HttpServlet; 49 import javax.servlet.http.HttpServletRequest; 50 import javax.servlet.http.HttpServletResponse; 51 52 /** Test inactivity notification job. */ 53 public class VtsInactivityJobServlet extends BaseJobServlet { 54 private static final String INACTIVITY_ALERT_URL = "/cron/vts_inactivity_job"; 55 protected static final Logger logger = 56 Logger.getLogger(VtsInactivityJobServlet.class.getName()); 57 58 /** 59 * Compose an email if the test is inactive. 60 * 61 * @param test The TestStatusEntity document storing the test status. 62 * @param lastRunTime The timestamp in microseconds of the last test run for this test. 63 * @param link Fully specified link to the test's status page. 64 * @param emails The list of email addresses to send the email. 65 * @param messages The message list in which to insert the inactivity notification email. 66 * @return True if the test is inactive, false otherwise. 67 */ notifyIfInactive( TestStatusEntity test, long lastRunTime, String link, List<String> emails, List<Message> messages)68 private static boolean notifyIfInactive( 69 TestStatusEntity test, 70 long lastRunTime, 71 String link, 72 List<String> emails, 73 List<Message> messages) { 74 long now = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()); 75 long diff = now - lastRunTime; 76 // Send an email daily to notify that the test hasn't been running. 77 // After 7 full days have passed, notifications will no longer be sent (i.e. the 78 // test is assumed to be deprecated). 79 if (diff >= TimeUnit.DAYS.toMicros(1) && diff < TimeUnit.DAYS.toMicros(8)) { 80 String uploadTimeString = TimeUtil.getDateTimeZoneString(lastRunTime); 81 String subject = "Warning! Inactive test: " + test.getTestName(); 82 String body = 83 "Hello,<br><br>Test \"" 84 + test.getTestName() 85 + "\" is inactive. " 86 + "No new data has been uploaded since " 87 + uploadTimeString 88 + "." 89 + EmailHelper.getEmailFooter(null, null, link); 90 try { 91 messages.add(EmailHelper.composeEmail(emails, subject, body)); 92 return true; 93 } catch (MessagingException | UnsupportedEncodingException e) { 94 logger.log(Level.WARNING, "Error composing email : ", e); 95 } 96 } 97 return false; 98 } 99 100 /** 101 * Get the timestamp for the last test run for the specified test. 102 * 103 * @param testKey The parent key of the test runs to query for. 104 * @return The timestamp in microseconds of the last test run for the test, or -1 if none. 105 */ getLastRunTime(Key testKey)106 private static long getLastRunTime(Key testKey) { 107 DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); 108 Filter testTypeFilter = FilterUtil.getTestTypeFilter(false, true, false); 109 Query q = 110 new Query(TestRunEntity.KIND) 111 .setAncestor(testKey) 112 .setFilter(testTypeFilter) 113 .addSort(Entity.KEY_RESERVED_PROPERTY, SortDirection.DESCENDING) 114 .setKeysOnly(); 115 116 long lastTestRun = -1; 117 for (Entity testRun : datastore.prepare(q).asIterable(FetchOptions.Builder.withLimit(1))) { 118 lastTestRun = testRun.getKey().getId(); 119 } 120 return lastTestRun; 121 } 122 123 @Override doGet(HttpServletRequest request, HttpServletResponse response)124 public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { 125 DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); 126 Queue queue = QueueFactory.getDefaultQueue(); 127 Query q = new Query(TestStatusEntity.KIND).setKeysOnly(); 128 List<TaskOptions> tasks = new ArrayList<>(); 129 for (Entity status : datastore.prepare(q).asIterable()) { 130 TaskOptions task = 131 TaskOptions.Builder.withUrl(INACTIVITY_ALERT_URL) 132 .param("statusKey", KeyFactory.keyToString(status.getKey())) 133 .method(TaskOptions.Method.POST); 134 tasks.add(task); 135 } 136 TaskQueueHelper.addToQueue(queue, tasks); 137 } 138 139 @Override doPost(HttpServletRequest request, HttpServletResponse response)140 public void doPost(HttpServletRequest request, HttpServletResponse response) 141 throws IOException { 142 DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); 143 String statusKeyString = request.getParameter("statusKey"); 144 145 Key statusKey; 146 try { 147 statusKey = KeyFactory.stringToKey(statusKeyString); 148 } catch (IllegalArgumentException e) { 149 logger.log(Level.WARNING, "Invalid key specified: " + statusKeyString); 150 return; 151 } 152 153 TestStatusEntity status = null; 154 try { 155 status = TestStatusEntity.fromEntity(datastore.get(statusKey)); 156 } catch (EntityNotFoundException e) { 157 // no existing status 158 } 159 if (status == null) { 160 return; 161 } 162 Key testKey = KeyFactory.createKey(TestEntity.KIND, status.getTestName()); 163 long lastRunTime = getLastRunTime(testKey); 164 165 StringBuffer fullUrl = request.getRequestURL(); 166 String baseUrl = fullUrl.substring(0, fullUrl.indexOf(request.getRequestURI())); 167 String link = baseUrl + "/show_tree?testName=" + status.getTestName(); 168 169 List<Message> messageQueue = new ArrayList<>(); 170 List<String> emails; 171 try { 172 emails = EmailHelper.getSubscriberEmails(testKey); 173 } catch (IOException e) { 174 logger.log(Level.SEVERE, e.toString()); 175 return; 176 } 177 notifyIfInactive(status, lastRunTime, link, emails, messageQueue); 178 if (messageQueue.size() > 0) { 179 EmailHelper.sendAll(messageQueue); 180 } 181 } 182 } 183