001/** 002 * Copyright 2005-2016 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.kcb.quartz; 017 018import java.sql.Timestamp; 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.Iterator; 022import java.util.List; 023import java.util.concurrent.Callable; 024import java.util.concurrent.ExecutorService; 025import java.util.concurrent.Executors; 026import java.util.concurrent.Future; 027import java.util.concurrent.ThreadFactory; 028 029import javax.persistence.OptimisticLockException; 030 031import org.apache.log4j.Logger; 032import org.kuali.rice.core.api.util.RiceUtilities; 033import org.kuali.rice.kcb.quartz.ProcessingResult.Failure; 034import org.springframework.beans.factory.annotation.Required; 035import org.springframework.dao.DataAccessException; 036import org.springframework.dao.OptimisticLockingFailureException; 037import org.springframework.transaction.PlatformTransactionManager; 038import org.springframework.transaction.TransactionException; 039import org.springframework.transaction.TransactionStatus; 040import org.springframework.transaction.UnexpectedRollbackException; 041import org.springframework.transaction.support.TransactionCallback; 042import org.springframework.transaction.support.TransactionTemplate; 043 044/** 045 * Base class for jobs that must obtain a set of work items atomically 046 * @author Kuali Rice Team (rice.collab@kuali.org) 047 */ 048public abstract class ConcurrentJob<T> { 049 protected final Logger LOG = Logger.getLogger(getClass()); 050 051 protected ExecutorService executor = Executors.newSingleThreadExecutor(new KCBThreadFactory()); 052 protected PlatformTransactionManager txManager; 053 054 /** 055 * Sets the {@link ExecutorService} to use to process work items. Default is single-threaded. 056 * @param executor the {@link ExecutorService} to use to process work items. Default is single-threaded. 057 */ 058 public void setExecutorService(ExecutorService executor) { 059 this.executor = executor; 060 } 061 062 /** 063 * Sets the {@link PlatformTransactionManager} 064 * @param txManager the {@link PlatformTransactionManager} 065 */ 066 @Required 067 public void setTransactionManager(PlatformTransactionManager txManager) { 068 this.txManager = txManager; 069 } 070 071 /** 072 * Helper method for creating a TransactionTemplate initialized to create 073 * a new transaction 074 * @return a TransactionTemplate initialized to create a new transaction 075 */ 076 protected TransactionTemplate createNewTransaction() { 077 TransactionTemplate tt = new TransactionTemplate(txManager); 078 tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); 079 return tt; 080 } 081 082 /** 083 * Template method that subclasses should override to obtain a set of available work items 084 * and mark them as taken 085 * @return a collection of available work items that have been marked as taken 086 */ 087 protected abstract Collection<T> takeAvailableWorkItems(); 088 089 /** 090 * Template method that subclasses should override to group work items into units of work 091 * @param workItems list of work items to break into groups 092 * @param result ProcessingResult to modify if there are any failures...this is sort of a hack because previously 093 * failure to obtain a deliverer was considered a work item failure, and now this method has been factored out... 094 * but the tests still want to see the failure 095 * @return a collection of collection of work items 096 */ 097 protected Collection<Collection<T>> groupWorkItems(Collection<T> workItems, ProcessingResult<T> result) { 098 Collection<Collection<T>> groupedWorkItems = new ArrayList<Collection<T>>(workItems.size()); 099 for (T workItem: workItems) { 100 Collection<T> c = new ArrayList<T>(1); 101 c.add(workItem); 102 groupedWorkItems.add(c); 103 } 104 return groupedWorkItems; 105 } 106 107 /** 108 * Template method that subclasses should override to process a given work item and mark it 109 * as untaken afterwards 110 * @param item the work item 111 * @return a collection of success messages 112 */ 113 protected abstract Collection<T> processWorkItems(Collection<T> items); 114 115 /** 116 * Template method that subclasses should override to unlock a given work item when procesing has failed. 117 * @param item the work item to unlock 118 */ 119 protected abstract void unlockWorkItem(T item); 120 121 /** 122 * Main processing method which invokes subclass implementations of template methods 123 * to obtain available work items, and process them concurrently 124 * @return a ProcessingResult object containing the results of processing 125 */ 126 public ProcessingResult<T> run() { 127 if ( LOG.isDebugEnabled() ) { 128 LOG.debug("[" + new Timestamp(System.currentTimeMillis()).toString() + "] STARTING RUN"); 129 } 130 131 final ProcessingResult<T> result = new ProcessingResult<T>(); 132 133 // retrieve list of available work items in a transaction 134 final Collection<T> items; 135 try { 136 items = executeInTransaction(new TransactionCallback() { 137 public Object doInTransaction(TransactionStatus txStatus) { 138 return takeAvailableWorkItems(); 139 } 140 }); 141 } catch (DataAccessException dae) { 142 Exception ole = RiceUtilities.findExceptionInStack(dae, OptimisticLockingFailureException.class); 143 if ( ole == null ) { 144 ole = RiceUtilities.findExceptionInStack(dae, OptimisticLockException.class); 145 } 146 147 if (ole != null) { 148 // anticipated in the case that another thread is trying to grab items 149 LOG.info("Contention while taking work items: " + ole.getMessage() ); 150 } else { 151 // in addition to logging a message, should we throw an exception or log a failure here? 152 LOG.error("Error taking work items", dae); 153 } 154 return result; 155 } catch (UnexpectedRollbackException ure) { 156 LOG.error("UnexpectedRollbackException", ure); 157 return result; 158 } catch (TransactionException te) { 159 LOG.error("Error occurred obtaining available work items", te); 160 result.addFailure(new Failure<T>(te)); 161 return result; 162 } 163 164 Collection<Collection<T>> groupedWorkItems = groupWorkItems(items, result); 165 166 // now iterate over all work groups and process each 167 Iterator<Collection<T>> i = groupedWorkItems.iterator(); 168 List<Future> futures = new ArrayList<Future>(); 169 while(i.hasNext()) { 170 final Collection<T> workUnit= i.next(); 171 172 LOG.info("Processing work unit: " + workUnit); 173 /* performed within transaction */ 174 /* executor manages threads to run work items... */ 175 futures.add(executor.submit(new Callable() { 176 public Object call() throws Exception { 177 ProcessingResult<T> result = new ProcessingResult<T>(); 178 try { 179 Collection<T> successes = executeInTransaction(new TransactionCallback() { 180 public Object doInTransaction(TransactionStatus txStatus) { 181 return processWorkItems(workUnit); 182 } 183 }); 184 result.addAllSuccesses(successes); 185 } catch (Exception e) { 186 LOG.error("Error occurred processing work unit " + workUnit, e); 187 for (final T workItem: workUnit) { 188 LOG.error("Error occurred processing work item " + workItem, e); 189 result.addFailure(new Failure<T>(workItem, e)); 190 unlockWorkItemAtomically(workItem); 191 } 192 } 193 return result; 194 } 195 })); 196 } 197 198 // wait for workers to finish 199 for (Future f: futures) { 200 try { 201 ProcessingResult<T> workResult = (ProcessingResult<T>) f.get(); 202 result.add(workResult); 203 } catch (Exception e) { 204 String message = "Error obtaining work result: " + e; 205 LOG.error(message, e); 206 result.addFailure(new Failure<T>(e, message)); 207 } 208 } 209 210 finishProcessing(result); 211 212 if ( LOG.isDebugEnabled() ) { 213 LOG.debug("[" + new Timestamp(System.currentTimeMillis()).toString() + "] FINISHED RUN - " + result); 214 } 215 216 return result; 217 } 218 219 /** 220 * Template method called after processing of work items has completed 221 */ 222 protected void finishProcessing(ProcessingResult<T> result) {} 223 224 protected void unlockWorkItemAtomically(final T workItem) { 225 try { 226 executeInTransaction(new TransactionCallback() { 227 public Object doInTransaction(TransactionStatus txStatus) { 228 LOG.info("Unlocking failed work item: " + workItem); 229 unlockWorkItem(workItem); 230 return null; 231 } 232 }); 233 } catch (Exception e2) { 234 LOG.error("Error unlocking failed work item " + workItem, e2); 235 } 236 } 237 238 protected <X> X executeInTransaction(TransactionCallback callback) { 239 // haha just kidding 240 //return (X) callback.doInTransaction(null); 241 return (X) createNewTransaction().execute(callback); 242 } 243 244 private static class KCBThreadFactory implements ThreadFactory { 245 246 private ThreadFactory defaultThreadFactory = Executors.defaultThreadFactory(); 247 248 public Thread newThread(Runnable runnable) { 249 Thread thread = defaultThreadFactory.newThread(runnable); 250 thread.setName("KCB-job-" + thread.getName()); 251 return thread; 252 } 253 } 254}