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