/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.ode.bpel.engine; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.ode.bpel.dao.MessageExchangeDAO; import org.apache.ode.bpel.dao.ProcessDAO; import org.apache.ode.bpel.dao.ProcessInstanceDAO; import org.apache.ode.bpel.evt.BpelEvent; import org.apache.ode.bpel.iapi.BpelEngine; import org.apache.ode.bpel.iapi.BpelEngineException; import org.apache.ode.bpel.iapi.ContextException; import org.apache.ode.bpel.iapi.Endpoint; import org.apache.ode.bpel.iapi.Message; import org.apache.ode.bpel.iapi.MessageExchange; import org.apache.ode.bpel.iapi.MessageExchange.MessageExchangePattern; import org.apache.ode.bpel.iapi.MessageExchange.Status; import org.apache.ode.bpel.iapi.MyRoleMessageExchange; import org.apache.ode.bpel.iapi.MyRoleMessageExchange.CorrelationStatus; import org.apache.ode.bpel.iapi.Scheduler; import org.apache.ode.bpel.iapi.Scheduler.JobInfo; import org.apache.ode.bpel.intercept.MessageExchangeInterceptor; import org.apache.ode.bpel.o.OPartnerLink; import org.apache.ode.bpel.o.OProcess; import org.apache.ode.bpel.runtime.history.ODEBpelProcess; import org.apache.ode.utils.msg.MessageBundle; import tud.st.bpel.history.BpelHistory; import javax.wsdl.Operation; import javax.wsdl.PortType; import javax.xml.namespace.QName; import java.io.File; import java.io.FileOutputStream; import java.io.ObjectOutputStream; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; /** * Implementation of the {@link BpelEngine} interface: provides the server methods that should be invoked in the context of a * transaction. * * @author mszefler * @author Matthieu Riou <mriou at apache dot org> */ public class BpelEngineImpl implements BpelEngine { private static final Log __log = LogFactory.getLog(BpelEngineImpl.class); private final BpelHistory history = BpelHistory.getInstance(); /** RNG, for delays */ private Random _random = new Random(System.currentTimeMillis()); private static double _delayMean = 0; static { try { String delay = System.getenv("ODE_DEBUG_TX_DELAY"); if (delay != null && delay.length() > 0) { _delayMean = Double.valueOf(delay); __log.info("Stochastic debugging delay activated. Delay (Mean)=" + _delayMean + "ms."); } } catch (Throwable t) { if (__log.isDebugEnabled()) { __log.debug("Could not read ODE_DEBUG_TX_DELAY environment variable; assuming 0 (mean) delay", t); } else { __log.info("Could not read ODE_DEBUG_TX_DELAY environment variable; assuming 0 (mean) delay"); } } } private static final Messages __msgs = MessageBundle.getMessages(Messages.class); /** Maximum number of tries for async jobs. */ private static final int MAX_RETRIES = 3; /** Active processes, keyed by process id. */ final HashMap<QName, BpelProcess> _activeProcesses = new HashMap<QName, BpelProcess>(); /** Mapping from myrole endpoint name to active process. */ private final HashMap<Endpoint, BpelProcess> _serviceMap = new HashMap<Endpoint, BpelProcess>(); /** Manage instance-level locks. */ private final InstanceLockManager _instanceLockManager = new InstanceLockManager(); final Contexts _contexts; public BpelEngineImpl(Contexts contexts) { _contexts = contexts; } public MyRoleMessageExchange createMessageExchange(String clientKey, QName targetService, String operation, String pipedMexId) throws BpelEngineException { BpelProcess target = route(targetService, null); MessageExchangeDAO dao; if (target == null || target.isInMemory()) { dao = _contexts.inMemDao.getConnection().createMessageExchange(MessageExchangeDAO.DIR_PARTNER_INVOKES_MYROLE); } else { dao = _contexts.dao.getConnection().createMessageExchange(MessageExchangeDAO.DIR_PARTNER_INVOKES_MYROLE); } dao.setCorrelationId(clientKey); dao.setCorrelationStatus(CorrelationStatus.UKNOWN_ENDPOINT.toString()); dao.setPattern(MessageExchangePattern.UNKNOWN.toString()); dao.setCallee(targetService); dao.setStatus(Status.NEW.toString()); dao.setOperation(operation); dao.setPipedMessageExchangeId(pipedMexId); MyRoleMessageExchangeImpl mex = new MyRoleMessageExchangeImpl(this, dao); if (target != null) { target.initMyRoleMex(mex); } return mex; } public MyRoleMessageExchange createMessageExchange(String clientKey, QName targetService, String operation) { return createMessageExchange(clientKey, targetService, operation, null); } public MessageExchange getMessageExchange(String mexId) throws BpelEngineException { MessageExchangeDAO mexdao = _contexts.inMemDao.getConnection().getMessageExchange(mexId); if (mexdao == null) mexdao = _contexts.dao.getConnection().getMessageExchange(mexId); if (mexdao == null) return null; ProcessDAO pdao = mexdao.getProcess(); BpelProcess process = pdao == null ? null : _activeProcesses.get(pdao.getProcessId()); MessageExchangeImpl mex; switch (mexdao.getDirection()) { case MessageExchangeDAO.DIR_BPEL_INVOKES_PARTNERROLE: if (process == null) { String errmsg = __msgs.msgProcessNotActive(pdao.getProcessId()); __log.error(errmsg); // TODO: Perhaps we should define a checked exception for this // condition. throw new BpelEngineException(errmsg); } { OPartnerLink plink = (OPartnerLink) process.getOProcess().getChild(mexdao.getPartnerLinkModelId()); PortType ptype = plink.partnerRolePortType; Operation op = plink.getPartnerRoleOperation(mexdao.getOperation()); // TODO: recover Partner's EPR mex = new PartnerRoleMessageExchangeImpl(this, mexdao, ptype, op, null, plink.hasMyRole() ? process .getInitialMyRoleEPR(plink) : null, process.getPartnerRoleChannel(plink)); } break; case MessageExchangeDAO.DIR_PARTNER_INVOKES_MYROLE: mex = new MyRoleMessageExchangeImpl(this, mexdao); if (process != null) { OPartnerLink plink = (OPartnerLink) process.getOProcess().getChild(mexdao.getPartnerLinkModelId()); PortType ptype = plink.myRolePortType; Operation op = plink.getMyRoleOperation(mexdao.getOperation()); mex.setPortOp(ptype, op); } break; default: String errmsg = "BpelEngineImpl: internal error, invalid MexDAO direction: " + mexId; __log.fatal(errmsg); throw new BpelEngineException(errmsg); } return mex; } BpelProcess unregisterProcess(QName process) { BpelProcess p = _activeProcesses.remove(process); if (p != null) { if (__log.isDebugEnabled()) __log.debug("Deactivating process " + p.getPID()); Endpoint processEndpoint = null; Iterator<Map.Entry<Endpoint,BpelProcess>> serviceIter = _serviceMap.entrySet().iterator(); while (serviceIter.hasNext()) { Map.Entry<Endpoint,BpelProcess> processEntry = serviceIter.next(); if (processEntry.getValue()._pid.equals(process)) { serviceIter.remove(); processEndpoint = processEntry.getKey(); } } // Only deactivating if no other process (version) need that endpoint anymore // We're only routing using an endpoint/process map for now which means that deploying // several versions of the same process using the same endpoint (which is the common // case) will override previous deployments endpoints. So checking the endpoint is not // enough, we also have to check other versions of the same process. // A bit clunky, the maps held here should be retought a bit. boolean otherVersions = false; for (BpelProcess bpelProcess : _activeProcesses.values()) { if (bpelProcess._pconf.getType().equals(p._pconf.getType())) otherVersions = true; } if (_serviceMap.get(processEndpoint) == null && !otherVersions) { p.deactivate(); } } return p; } boolean isProcessRegistered(QName pid) { return _activeProcesses.containsKey(pid); } /** * Register a process with the engine. * @param process the process to register */ void registerProcess(BpelProcess process) { _activeProcesses.put(process.getPID(), process); for (Endpoint e : process.getServiceNames()) { __log.debug("Register process: serviceId=" + e + ", process=" + process); _serviceMap.put(e, process); } process.activate(this); // AL: Add static process facts history.registerProcess(new ODEBpelProcess(process.getOProcess())); } /** * Route to a process using the service id. Note, that we do not need the endpoint name here, we are assuming that two processes * would not be registered under the same service qname but different endpoint. * * @param service * target service id * @param request * request message * @return process corresponding to the targetted service, or <code>null</code> if service identifier is not recognized. */ BpelProcess route(QName service, Message request) { // TODO: use the message to route to the correct service if more than // one service is listening on the same endpoint. BpelProcess routed = null; for (Endpoint endpoint : _serviceMap.keySet()) { if (endpoint.serviceName.equals(service)) routed = _serviceMap.get(endpoint); } if (__log.isDebugEnabled()) __log.debug("Routed: svcQname " + service + " --> " + routed); return routed; } OProcess getOProcess(QName processId) { BpelProcess process = _activeProcesses.get(processId); if (process == null) return null; return process.getOProcess(); } public void onScheduledJob(Scheduler.JobInfo jobInfo) throws Scheduler.JobProcessorException { final WorkEvent we = new WorkEvent(jobInfo.jobDetail); // We lock the instance to prevent concurrent transactions and prevent unnecessary rollbacks, // Note that we don't want to wait too long here to get our lock, since we are likely holding // on to scheduler's locks of various sorts. try { _instanceLockManager.lock(we.getIID(), 1, TimeUnit.MICROSECONDS); _contexts.scheduler.registerSynchronizer(new Scheduler.Synchronizer() { public void afterCompletion(boolean success) { _instanceLockManager.unlock(we.getIID()); } public void beforeCompletion() { } }); } catch (InterruptedException e) { // Retry later. __log.debug("Thread interrupted, job will be rescheduled: " + jobInfo); throw new Scheduler.JobProcessorException(true); } catch (org.apache.ode.bpel.engine.InstanceLockManager.TimeoutException e) { __log.debug("Instance " + we.getIID() + " is busy, rescheduling job."); // TODO: This should really be more of something like the exponential backoff algorithm in ethernet. _contexts.scheduler.schedulePersistedJob(jobInfo.jobDetail, new Date(System.currentTimeMillis() + Math.min(randomExp(1000), 10000))); return; } // DONT PUT CODE HERE-need this method real tight in a try/catch block, we need to handle // all types of failure here, the scheduler is not going to know how to handle our errors, // ALSO we have to release the lock obtained above (IMPORTANT), lest the whole system come // to a grinding halt. try { BpelProcess process; if (we.getProcessId() != null) { process = _activeProcesses.get(we.getProcessId()); } else { ProcessInstanceDAO instance; if (we.isInMem()) instance = _contexts.inMemDao.getConnection().getInstance(we.getIID()); else instance = _contexts.dao.getConnection().getInstance(we.getIID()); if (instance == null) { __log.debug(__msgs.msgScheduledJobReferencesUnknownInstance(we.getIID())); // nothing we can do, this instance is not in the database, it will always fail, not // exactly an error since can occur in normal course of events. return; } ProcessDAO processDao = instance.getProcess(); process = _activeProcesses.get(processDao.getProcessId()); } if (process == null) { // If the process is not active, it means that we should not be // doing any work on its behalf, therefore we will reschedule the // events for some time in the future (1 minute). Date future = new Date(System.currentTimeMillis() + (60 * 1000)); __log.info(__msgs.msgReschedulingJobForInactiveProcess(we.getProcessId(), jobInfo.jobName, future)); _contexts.scheduler.schedulePersistedJob(jobInfo.jobDetail, future); return; } process.handleWorkEvent(jobInfo.jobDetail); debuggingDelay(); } catch (BpelEngineException bee) { __log.error(__msgs.msgScheduledJobFailed(we.getDetail()), bee); throw new Scheduler.JobProcessorException(bee, checkRetry(jobInfo, bee)); } catch (ContextException ce) { __log.error(__msgs.msgScheduledJobFailed(we.getDetail()), ce); throw new Scheduler.JobProcessorException(ce, checkRetry(jobInfo, ce)); } catch (RuntimeException rte) { __log.error(__msgs.msgScheduledJobFailed(we.getDetail()), rte); throw new Scheduler.JobProcessorException(rte, checkRetry(jobInfo, rte)); } catch (Throwable t) { __log.error(__msgs.msgScheduledJobFailed(we.getDetail()), t); throw new Scheduler.JobProcessorException(false); } } private boolean checkRetry(final JobInfo jobInfo, Throwable t) { // TODO, better handling of failed jobs (put them in the DB perhaps?) if (jobInfo.retryCount < MAX_RETRIES) return true; __log.error("Job could not be completed after " + MAX_RETRIES + ": " + jobInfo, t); boolean saveToDisk = false; if (jobInfo.jobDetail.get("final") == null) { __log.error("Rescheduling problematic job for a bit later: " + jobInfo, t); try { if (jobInfo.jobDetail.get("inmem") != null) _contexts.scheduler.scheduleVolatileJob(true, jobInfo.jobDetail); else _contexts.scheduler.execIsolatedTransaction(new Callable<Void>() { public Void call() throws Exception { jobInfo.jobDetail.put("final", true); _contexts.scheduler.schedulePersistedJob(jobInfo.jobDetail, new Date(System.currentTimeMillis() + 60 * 1000)); return null; } }); } catch (Exception ex) { __log.error("Error rescheduling problematic job: " + jobInfo,ex); saveToDisk = true; } } else { saveToDisk = true; } if (saveToDisk) try { File f = File.createTempFile("ode-bad-job", ".ser", new File("")); ObjectOutputStream fos = new ObjectOutputStream(new FileOutputStream(f)); fos.writeObject(jobInfo); fos.close(); __log.error("Saved problematic job to disk (last resort): " + jobInfo +" in file " + f); } catch (Exception ex) { __log.error("Could not save bad job; it will be lost: " + jobInfo, ex); } // No more retries. return false; } /** * Block the thread for random amount of time. Used for testing for races and the like. The delay generated is exponentially * distributed with the mean obtained from the <code>ODE_DEBUG_TX_DELAY</code> environment variable. */ private void debuggingDelay() { // Do a delay for debugging purposes. if (_delayMean != 0) try { long delay = randomExp(_delayMean); // distribution // with mean // _delayMean __log.warn("Debugging delay has been activated; delaying transaction for " + delay + "ms."); Thread.sleep(delay); } catch (InterruptedException e) { ; // ignore } } private long randomExp(double mean) { double u = _random.nextDouble(); // Uniform long delay = (long) (-Math.log(u) * mean); // Exponential return delay; } void fireEvent(BpelEvent event) { // Note that the eventListeners list is a copy-on-write array, so need // to mess with synchronization. for (org.apache.ode.bpel.iapi.BpelEventListener l : _contexts.eventListeners) { l.onEvent(event); } } /** * Get the list of globally-registered message-exchange interceptors. * * @return list */ List<MessageExchangeInterceptor> getGlobalInterceptors() { return _contexts.globalIntereceptors; } }