001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018 019package org.apache.oozie.executor.jpa; 020 021import java.sql.Timestamp; 022import java.text.ParseException; 023import java.util.ArrayList; 024import java.util.Collections; 025import java.util.HashMap; 026import java.util.Iterator; 027import java.util.List; 028import java.util.Map; 029import java.util.Map.Entry; 030import java.util.regex.Matcher; 031import java.util.regex.Pattern; 032 033import javax.persistence.EntityManager; 034import javax.persistence.Query; 035 036import org.apache.oozie.BundleJobBean; 037import org.apache.oozie.client.BundleJob; 038import org.apache.oozie.client.CoordinatorJob; 039import org.apache.oozie.client.rest.BulkResponseImpl; 040import org.apache.oozie.BulkResponseInfo; 041import org.apache.oozie.CoordinatorActionBean; 042import org.apache.oozie.CoordinatorJobBean; 043import org.apache.oozie.ErrorCode; 044import org.apache.oozie.StringBlob; 045import org.apache.oozie.client.CoordinatorAction; 046import org.apache.oozie.service.Services; 047import org.apache.oozie.util.DateUtils; 048import org.apache.oozie.util.ParamChecker; 049 050/** 051 * The query executor class for bulk monitoring queries i.e. debugging bundle -> 052 * coord actions directly 053 */ 054public class BulkJPAExecutor implements JPAExecutor<BulkResponseInfo> { 055 private Map<String, List<String>> bulkFilter; 056 // defaults 057 private int start = 1; 058 private int len = 50; 059 private enum PARAM_TYPE { 060 ID, NAME 061 } 062 063 public BulkJPAExecutor(Map<String, List<String>> bulkFilter, int start, int len) { 064 ParamChecker.notNull(bulkFilter, "bulkFilter"); 065 this.bulkFilter = bulkFilter; 066 this.start = start; 067 this.len = len; 068 } 069 070 @Override 071 public String getName() { 072 return "BulkJPAExecutor"; 073 } 074 075 @Override 076 public BulkResponseInfo execute(EntityManager em) throws JPAExecutorException { 077 List<BulkResponseImpl> responseList = new ArrayList<BulkResponseImpl>(); 078 Map<String, Timestamp> actionTimes = new HashMap<String, Timestamp>(); 079 080 try { 081 // Lightweight Query 1 on Bundle level to fetch the bundle job(s) 082 // corresponding to names or ids 083 List<BundleJobBean> bundleBeans = bundleQuery(em); 084 085 // Join query between coordinator job and coordinator action tables 086 // to get entries for specific bundleId only 087 String conditions = actionQuery(em, bundleBeans, actionTimes, responseList); 088 089 // Query to get the count of records 090 long total = countQuery(conditions, em, bundleBeans, actionTimes); 091 092 BulkResponseInfo bulk = new BulkResponseInfo(responseList, start, len, total); 093 return bulk; 094 } 095 catch (Exception e) { 096 throw new JPAExecutorException(ErrorCode.E0603, e.getMessage(), e); 097 } 098 } 099 100 /** 101 * build the bundle level query to get bundle beans for the specified ids or appnames 102 * @param em 103 * @return List BundleJobBeans 104 * @throws JPAExecutorException 105 */ 106 @SuppressWarnings("unchecked") 107 private List<BundleJobBean> bundleQuery(EntityManager em) throws JPAExecutorException { 108 Query q = em.createNamedQuery("BULK_MONITOR_BUNDLE_QUERY"); 109 StringBuilder bundleQuery = new StringBuilder(q.toString()); 110 111 StringBuilder whereClause = null; 112 List<String> bundles = bulkFilter.get(BulkResponseImpl.BULK_FILTER_BUNDLE); 113 if (bundles != null) { 114 PARAM_TYPE type = getParamType(bundles.get(0), 'B'); 115 if (type == PARAM_TYPE.NAME) { 116 whereClause = inClause(bundles, "appName", 'b'); 117 } 118 else if (type == PARAM_TYPE.ID) { 119 whereClause = inClause(bundles, "id", 'b'); 120 } 121 122 // Query: select <columns> from BundleJobBean b where b.id IN (...) _or_ b.appName IN (...) 123 bundleQuery.append(whereClause.replace(whereClause.indexOf("AND"), whereClause.indexOf("AND") + 3, "WHERE")); 124 List<Object[]> bundleObjs = (List<Object[]>) em.createQuery(bundleQuery.toString()).getResultList(); 125 if (bundleObjs.isEmpty()) { 126 throw new JPAExecutorException(ErrorCode.E0603, "No entries found for given bundle(s)"); 127 } 128 129 List<BundleJobBean> bundleBeans = new ArrayList<BundleJobBean>(); 130 for (Object[] bundleElem : bundleObjs) { 131 bundleBeans.add(constructBundleBean(bundleElem)); 132 } 133 return bundleBeans; 134 } 135 return null; 136 } 137 138 /** 139 * Validate and determine whether passed param is job-id or appname 140 * @param id 141 * @param job 142 * @return PARAM_TYPE 143 */ 144 private PARAM_TYPE getParamType(String id, char job) { 145 Pattern p = Pattern.compile("\\d{7}-\\d{15}-" + Services.get().getSystemId() + "-" + job); 146 Matcher m = p.matcher(id); 147 if (m.matches()) { 148 return PARAM_TYPE.ID; 149 } 150 return PARAM_TYPE.NAME; 151 } 152 153 /** 154 * Compose the coord action level query comprising bundle id/appname filter and coord action 155 * status filter (if specified) and start-time or nominal-time filter (if specified) 156 * @param em 157 * @param bundles 158 * @param times 159 * @param responseList 160 * @return Query string 161 * @throws ParseException 162 */ 163 @SuppressWarnings("unchecked") 164 private String actionQuery(EntityManager em, List<BundleJobBean> bundles, 165 Map<String, Timestamp> times, List<BulkResponseImpl> responseList) throws ParseException { 166 Query q = em.createNamedQuery("BULK_MONITOR_ACTIONS_QUERY"); 167 StringBuilder getActions = new StringBuilder(q.toString()); 168 int offset = getActions.indexOf("ORDER"); 169 StringBuilder conditionClause = new StringBuilder(); 170 171 List<String> coords = bulkFilter.get(BulkResponseImpl.BULK_FILTER_COORD); 172 // Query: Select <columns> from CoordinatorActionBean a, CoordinatorJobBean c WHERE a.jobId = c.id 173 // AND c.bundleId = :bundleId AND c.appName/id IN (...) 174 if (coords != null) { 175 PARAM_TYPE type = getParamType(coords.get(0), 'C'); 176 if (type == PARAM_TYPE.NAME) { 177 conditionClause.append(inClause(bulkFilter.get(BulkResponseImpl.BULK_FILTER_COORD), "appName", 'c')); 178 } 179 else if (type == PARAM_TYPE.ID) { 180 conditionClause.append(inClause(bulkFilter.get(BulkResponseImpl.BULK_FILTER_COORD), "id", 'c')); 181 } 182 } 183 // Query: Select <columns> from CoordinatorActionBean a, CoordinatorJobBean c WHERE a.jobId = c.id 184 // AND c.bundleId = :bundleId AND c.appName/id IN (...) AND a.statusStr IN (...) 185 conditionClause.append(statusClause(bulkFilter.get(BulkResponseImpl.BULK_FILTER_STATUS))); 186 offset = getActions.indexOf("ORDER"); 187 getActions.insert(offset - 1, conditionClause); 188 189 // Query: Select <columns> from CoordinatorActionBean a, CoordinatorJobBean c WHERE a.jobId = c.id 190 // AND c.bundleId = :bundleId AND c.appName/id IN (...) AND a.statusStr IN (...) 191 // AND a.createdTimestamp >= startCreated _or_ a.createdTimestamp <= endCreated 192 // AND a.nominalTimestamp >= startNominal _or_ a.nominalTimestamp <= endNominal 193 timesClause(getActions, offset, times); 194 q = em.createQuery(getActions.toString()); 195 Iterator<Entry<String, Timestamp>> iter = times.entrySet().iterator(); 196 while (iter.hasNext()) { 197 Entry<String, Timestamp> time = iter.next(); 198 q.setParameter(time.getKey(), time.getValue()); 199 } 200 // pagination 201 q.setFirstResult(start - 1); 202 q.setMaxResults(len); 203 // repeatedly execute above query for each bundle 204 for (BundleJobBean bundle : bundles) { 205 q.setParameter("bundleId", bundle.getId()); 206 List<Object[]> response = q.getResultList(); 207 for (Object[] r : response) { 208 BulkResponseImpl br = getResponseFromObject(bundle, r); 209 responseList.add(br); 210 } 211 } 212 return q.toString(); 213 } 214 215 /** 216 * Get total number of records for use with offset and len in API 217 * @param clause 218 * @param em 219 * @param bundles 220 * @return total count of coord actions 221 */ 222 private long countQuery(String clause, EntityManager em, List<BundleJobBean> bundles, Map<String, Timestamp> times) { 223 Query q = em.createNamedQuery("BULK_MONITOR_COUNT_QUERY"); 224 StringBuilder getTotal = new StringBuilder(q.toString() + " "); 225 // Query: select COUNT(a) from CoordinatorActionBean a, CoordinatorJobBean c 226 // get entire WHERE clause from above i.e. actionQuery() for all conditions on coordinator job 227 // and action status and times 228 getTotal.append(clause.substring(clause.indexOf("WHERE"), clause.indexOf("ORDER"))); 229 int offset = getTotal.indexOf("bundleId"); 230 List<String> bundleIds = new ArrayList<String>(); 231 for (BundleJobBean bundle : bundles) { 232 bundleIds.add(bundle.getId()); 233 } 234 // Query: select COUNT(a) from CoordinatorActionBean a, CoordinatorJobBean c WHERE ... 235 // AND c.bundleId IN (... list of bundle ids) i.e. replace single :bundleId with list 236 getTotal = getTotal.replace(offset - 6, offset + 20, inClause(bundleIds, "bundleId", 'c').toString()); 237 q = em.createQuery(getTotal.toString()); 238 Iterator<Entry<String, Timestamp>> iter = times.entrySet().iterator(); 239 while (iter.hasNext()) { 240 Entry<String, Timestamp> time = iter.next(); 241 q.setParameter(time.getKey(), time.getValue()); 242 } 243 long total = ((Long) q.getSingleResult()).longValue(); 244 return total; 245 } 246 247 // Form the where clause to filter by coordinator appname/id 248 private StringBuilder inClause(List<String> values, String col, char type) { 249 StringBuilder sb = new StringBuilder(); 250 boolean firstVal = true; 251 for (String name : nullToEmpty(values)) { 252 if (firstVal) { 253 sb.append(" AND " + type + "." + col + " IN (\'" + name + "\'"); 254 firstVal = false; 255 } 256 else { 257 sb.append(",\'" + name + "\'"); 258 } 259 } 260 if (!firstVal) { 261 sb.append(") "); 262 } 263 return sb; 264 } 265 266 // Form the where clause to filter by coord action status 267 private StringBuilder statusClause(List<String> statuses) { 268 StringBuilder sb = inClause(statuses, "statusStr", 'a'); 269 if (sb.length() == 0) { // statuses was null. adding default 270 sb.append(" AND a.statusStr IN ('KILLED', 'FAILED') "); 271 } 272 return sb; 273 } 274 275 private void timesClause(StringBuilder sb, int offset, Map<String, Timestamp> eachTime) throws ParseException { 276 Timestamp ts = null; 277 List<String> times = bulkFilter.get(BulkResponseImpl.BULK_FILTER_START_CREATED_EPOCH); 278 if (times != null) { 279 ts = new Timestamp(DateUtils.parseDateUTC(times.get(0)).getTime()); 280 sb.insert(offset - 1, " AND a.createdTimestamp >= :startCreated"); 281 eachTime.put("startCreated", ts); 282 } 283 times = bulkFilter.get(BulkResponseImpl.BULK_FILTER_END_CREATED_EPOCH); 284 if (times != null) { 285 ts = new Timestamp(DateUtils.parseDateUTC(times.get(0)).getTime()); 286 sb.insert(offset - 1, " AND a.createdTimestamp <= :endCreated"); 287 eachTime.put("endCreated", ts); 288 } 289 times = bulkFilter.get(BulkResponseImpl.BULK_FILTER_START_NOMINAL_EPOCH); 290 if (times != null) { 291 ts = new Timestamp(DateUtils.parseDateUTC(times.get(0)).getTime()); 292 sb.insert(offset - 1, " AND a.nominalTimestamp >= :startNominal"); 293 eachTime.put("startNominal", ts); 294 } 295 times = bulkFilter.get(BulkResponseImpl.BULK_FILTER_END_NOMINAL_EPOCH); 296 if (times != null) { 297 ts = new Timestamp(DateUtils.parseDateUTC(times.get(0)).getTime()); 298 sb.insert(offset - 1, " AND a.nominalTimestamp <= :endNominal"); 299 eachTime.put("endNominal", ts); 300 } 301 } 302 303 private BulkResponseImpl getResponseFromObject(BundleJobBean bundleBean, Object arr[]) { 304 BulkResponseImpl bean = new BulkResponseImpl(); 305 CoordinatorJobBean coordBean = new CoordinatorJobBean(); 306 CoordinatorActionBean actionBean = new CoordinatorActionBean(); 307 if (arr[0] != null) { 308 actionBean.setId((String) arr[0]); 309 } 310 if (arr[1] != null) { 311 actionBean.setActionNumber((Integer) arr[1]); 312 } 313 if (arr[2] != null) { 314 actionBean.setErrorCode((String) arr[2]); 315 } 316 if (arr[3] != null) { 317 actionBean.setErrorMessage((String) arr[3]); 318 } 319 if (arr[4] != null) { 320 actionBean.setExternalId((String) arr[4]); 321 } 322 if (arr[5] != null) { 323 actionBean.setExternalStatus((String) arr[5]); 324 } 325 if (arr[6] != null) { 326 actionBean.setStatus(CoordinatorAction.Status.valueOf((String) arr[6])); 327 } 328 if (arr[7] != null) { 329 actionBean.setCreatedTime(DateUtils.toDate((Timestamp) arr[7])); 330 } 331 if (arr[8] != null) { 332 actionBean.setNominalTime(DateUtils.toDate((Timestamp) arr[8])); 333 } 334 if (arr[9] != null) { 335 actionBean.setMissingDependenciesBlob((StringBlob) arr[9]); 336 } 337 if (arr[10] != null) { 338 coordBean.setId((String) arr[10]); 339 actionBean.setJobId((String) arr[10]); 340 } 341 if (arr[11] != null) { 342 coordBean.setAppName((String) arr[11]); 343 } 344 if (arr[12] != null) { 345 coordBean.setStatus(CoordinatorJob.Status.valueOf((String) arr[12])); 346 } 347 bean.setBundle(bundleBean); 348 bean.setCoordinator(coordBean); 349 bean.setAction(actionBean); 350 return bean; 351 } 352 353 private BundleJobBean constructBundleBean(Object[] barr) throws JPAExecutorException { 354 BundleJobBean bean = new BundleJobBean(); 355 if (barr[0] != null) { 356 bean.setId((String) barr[0]); 357 } 358 else { 359 throw new JPAExecutorException(ErrorCode.E0603, 360 "bundleId returned by query is null - cannot retrieve bulk results"); 361 } 362 if (barr[1] != null) { 363 bean.setAppName((String) barr[1]); 364 } 365 if (barr[2] != null) { 366 bean.setStatus(BundleJob.Status.valueOf((String) barr[2])); 367 } 368 if (barr[3] != null) { 369 bean.setUser((String) barr[3]); 370 } 371 return bean; 372 } 373 374 // null safeguard 375 public static List<String> nullToEmpty(List<String> input) { 376 return input == null ? Collections.<String> emptyList() : input; 377 } 378 379}