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.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}