001/** 002 * Copyright 2005-2017 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.ken.service.impl; 017 018import java.sql.SQLException; 019import java.sql.Timestamp; 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.Iterator; 023import java.util.List; 024import java.util.concurrent.Callable; 025import java.util.concurrent.ExecutorService; 026import java.util.concurrent.Future; 027 028import javax.persistence.OptimisticLockException; 029 030import org.apache.commons.lang.StringUtils; 031import org.apache.log4j.Logger; 032import org.kuali.rice.ken.service.ProcessingResult; 033import org.springframework.dao.DataAccessException; 034import org.springframework.dao.OptimisticLockingFailureException; 035import org.springframework.transaction.PlatformTransactionManager; 036import org.springframework.transaction.TransactionException; 037import org.springframework.transaction.TransactionStatus; 038import org.springframework.transaction.UnexpectedRollbackException; 039import org.springframework.transaction.support.TransactionCallback; 040import org.springframework.transaction.support.TransactionTemplate; 041 042/** 043 * Base class for jobs that must obtain a set of work items atomically 044 * @author Kuali Rice Team (rice.collab@kuali.org) 045 */ 046public abstract class ConcurrentJob<T> { 047 /** 048 * Oracle's "ORA-00054: resource busy and acquire with NOWAIT specified" 049 */ 050 private static final int ORACLE_00054 = 54; 051 /** 052 * Oracle's "ORA-00060 deadlock detected while waiting for resource" 053 */ 054 private static final int ORACLE_00060 = 60; 055 056 057 protected final Logger LOG = Logger.getLogger(getClass()); 058 059 protected ExecutorService executor; 060 protected PlatformTransactionManager txManager; 061 062 /** 063 * Constructs a ConcurrentJob instance. 064 * @param txManager PlatformTransactionManager to use for transactions 065 * @param executor the ExecutorService to use to process work items 066 */ 067 public ConcurrentJob(PlatformTransactionManager txManager, ExecutorService executor) { 068 this.txManager = txManager; 069 this.executor = executor; 070 } 071 072 /** 073 * Helper method for creating a TransactionTemplate initialized to create 074 * a new transaction 075 * @return a TransactionTemplate initialized to create a new transaction 076 */ 077 protected TransactionTemplate createNewTransaction() { 078 TransactionTemplate tt = new TransactionTemplate(txManager); 079 tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); 080 return tt; 081 } 082 083 /** 084 * Template method that subclasses should override to obtain a set of available work items 085 * and mark them as taken 086 * @return a collection of available work items that have been marked as taken 087 */ 088 protected abstract Collection<T> takeAvailableWorkItems(); 089 090 /** 091 * Template method that subclasses should override to group work items into units of work 092 * @param workItems list of work items to break into groups 093 * @param result ProcessingResult to modify if there are any failures...this is sort of a hack because previously 094 * failure to obtain a deliverer was considered a work item failure, and now this method has been factored out... 095 * but the tests still want to see the failure 096 * @return a collection of collection of work items 097 */ 098 protected Collection<Collection<T>> groupWorkItems(Collection<T> workItems, ProcessingResult result) { 099 Collection<Collection<T>> groupedWorkItems = new ArrayList<Collection<T>>(); 100 101 if (workItems != null) { 102 for (T workItem: workItems) { 103 Collection<T> c = new ArrayList<T>(1); 104 c.add(workItem); 105 groupedWorkItems.add(c); 106 } 107 } 108 return groupedWorkItems; 109 } 110 111 /** 112 * Template method that subclasses should override to process a given work item and mark it 113 * as untaken afterwards 114 * @param items the work item 115 * @return a collection of success messages 116 */ 117 protected abstract Collection<?> processWorkItems(Collection<T> items); 118 119 /** 120 * Template method that subclasses should override to unlock a given work item when procesing has failed. 121 * @param item the work item to unlock 122 */ 123 protected abstract void unlockWorkItem(T item); 124 125 /** 126 * Main processing method which invokes subclass implementations of template methods 127 * to obtain available work items, and process them concurrently 128 * @return a ProcessingResult object containing the results of processing 129 */ 130 @SuppressWarnings("unchecked") 131 public ProcessingResult run() { 132 if ( LOG.isDebugEnabled() ) { 133 LOG.debug("[" + new Timestamp(System.currentTimeMillis()).toString() + "] STARTING RUN"); 134 } 135 136 final ProcessingResult result = new ProcessingResult(); 137 138 // retrieve list of available work items in a transaction 139 Collection<T> items = null; 140 try { 141 items = (Collection<T>) 142 createNewTransaction().execute(new TransactionCallback() { 143 public Object doInTransaction(TransactionStatus txStatus) { 144 return takeAvailableWorkItems(); 145 } 146 }); 147 } catch (DataAccessException dae) { 148 if ( dae instanceof OptimisticLockingFailureException || dae.contains(OptimisticLockingFailureException.class) || dae.contains(OptimisticLockException.class) ) { 149 // anticipated in the case that another thread is trying to grab items 150 LOG.info("Contention while taking work items: " + dae.getMessage() ); 151 } else { 152 // in addition to logging a message, should we throw an exception or log a failure here? 153 LOG.error("Error taking work items", dae); 154 Throwable t = dae.getMostSpecificCause(); 155 if (t != null && t instanceof SQLException) { 156 SQLException sqle = (SQLException) t; 157 if (sqle.getErrorCode() == ORACLE_00054 && StringUtils.contains(sqle.getMessage(), "resource busy")) { 158 // this is expected and non-fatal given that these jobs will run again 159 LOG.warn("Select for update lock contention encountered: " + sqle.getMessage() ); 160 } else if (sqle.getErrorCode() == ORACLE_00060 && StringUtils.contains(sqle.getMessage(), "deadlock detected")) { 161 // this is bad...two parties are waiting forever somewhere... 162 // database is probably wedged now :( 163 LOG.error("Select for update deadlock encountered! " + sqle.getMessage() ); 164 } 165 } 166 } 167 return result; 168 } catch (UnexpectedRollbackException ure) { 169 LOG.error("UnexpectedRollbackException", ure); 170 return result; 171 } catch (TransactionException te) { 172 LOG.error("Error occurred obtaining available work items", te); 173 result.addFailure("Error occurred obtaining available work items: " + te); 174 return result; 175 } 176 177 Collection<Collection<T>> groupedWorkItems = groupWorkItems(items, result); 178 179 // now iterate over all work groups and process each 180 Iterator<Collection<T>> i = groupedWorkItems.iterator(); 181 List<Future> futures = new ArrayList<Future>(); 182 while(i.hasNext()) { 183 final Collection<T> workUnit= i.next(); 184 185 LOG.info("Processing work unit: " + workUnit); 186 /* performed within transaction */ 187 /* executor manages threads to run work items... */ 188 futures.add(executor.submit(new Callable() { 189 public Object call() throws Exception { 190 ProcessingResult result = new ProcessingResult(); 191 try { 192 Collection<?> successes = (Collection<Object>) 193 createNewTransaction().execute(new TransactionCallback() { 194 public Object doInTransaction(TransactionStatus txStatus) { 195 return processWorkItems(workUnit); 196 } 197 }); 198 result.addAllSuccesses(successes); 199 } catch (Exception e) { 200 LOG.error("Error occurred processing work unit " + workUnit, e); 201 for (final T workItem: workUnit) { 202 LOG.error("Error occurred processing work item " + workItem, e); 203 result.addFailure("Error occurred processing work item " + workItem + ": " + e); 204 unlockWorkItemAtomically(workItem); 205 } 206 } 207 return result; 208 } 209 })); 210 } 211 212 // wait for workers to finish 213 for (Future f: futures) { 214 try { 215 ProcessingResult workResult = (ProcessingResult) f.get(); 216 result.add(workResult); 217 } catch (Exception e) { 218 String message = "Error obtaining work result: " + e; 219 LOG.error(message, e); 220 result.addFailure(message); 221 } 222 } 223 224 if ( LOG.isDebugEnabled() ) { 225 LOG.debug("[" + new Timestamp(System.currentTimeMillis()).toString() + "] FINISHED RUN - " + result); 226 } 227 228 return result; 229 } 230 231 protected void unlockWorkItemAtomically(final T workItem) { 232 try { 233 createNewTransaction().execute(new TransactionCallback() { 234 public Object doInTransaction(TransactionStatus txStatus) { 235 LOG.info("Unlocking failed work item: " + workItem); 236 unlockWorkItem(workItem); 237 return null; 238 } 239 }); 240 } catch (Exception e2) { 241 LOG.error("Error unlocking failed work item " + workItem, e2); 242 } 243 } 244}