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}