/** * 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.cxf.ws.rm.soap; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.SortedSet; import java.util.TimerTask; import java.util.TreeSet; import java.util.concurrent.Executor; import java.util.concurrent.RejectedExecutionException; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.stream.XMLStreamReader; import org.w3c.dom.Node; import org.apache.cxf.Bus; import org.apache.cxf.binding.soap.SoapMessage; import org.apache.cxf.common.logging.LogUtils; import org.apache.cxf.endpoint.Endpoint; import org.apache.cxf.interceptor.Interceptor; import org.apache.cxf.interceptor.InterceptorChain; import org.apache.cxf.interceptor.InterceptorProvider; import org.apache.cxf.io.CachedOutputStream; import org.apache.cxf.message.Exchange; import org.apache.cxf.message.Message; import org.apache.cxf.phase.Phase; import org.apache.cxf.phase.PhaseInterceptorChain; import org.apache.cxf.phase.PhaseManager; import org.apache.cxf.staxutils.StaxSource; import org.apache.cxf.ws.rm.DestinationSequence; import org.apache.cxf.ws.rm.RMCaptureInInterceptor; import org.apache.cxf.ws.rm.RMContextUtils; import org.apache.cxf.ws.rm.RMManager; import org.apache.cxf.ws.rm.RMMessageConstants; import org.apache.cxf.ws.rm.RMProperties; import org.apache.cxf.ws.rm.RedeliveryQueue; import org.apache.cxf.ws.rm.RetryStatus; import org.apache.cxf.ws.rm.manager.RetryPolicyType; import org.apache.cxf.ws.rm.persistence.RMStore; import org.apache.cxf.ws.rm.v200702.Identifier; import org.apache.cxf.ws.rm.v200702.SequenceType; /** * */ public class RedeliveryQueueImpl implements RedeliveryQueue { private static final Logger LOG = LogUtils.getL7dLogger(RedeliveryQueueImpl.class); private Map<String, List<RedeliverCandidate>> candidates = new HashMap<String, List<RedeliverCandidate>>(); private Map<String, List<RedeliverCandidate>> suspendedCandidates = new HashMap<String, List<RedeliverCandidate>>(); private RMManager manager; private int undeliveredCount; public RedeliveryQueueImpl(RMManager m) { manager = m; } public RMManager getManager() { return manager; } public void setManager(RMManager m) { manager = m; } public void addUndelivered(Message message) { cacheUndelivered(message); } /** * @param seq the sequence under consideration * @return the number of undelivered messages for that sequence */ public synchronized int countUndelivered(DestinationSequence seq) { List<RedeliverCandidate> sequenceCandidates = getSequenceCandidates(seq); return sequenceCandidates == null ? 0 : sequenceCandidates.size(); } public int countUndelivered() { return undeliveredCount; } public boolean isEmpty() { return 0 == getUndelivered().size(); } public void purgeAll(DestinationSequence seq) { Collection<Long> purged = new ArrayList<>(); synchronized (this) { LOG.fine("Start purging redeliver candidates."); List<RedeliverCandidate> sequenceCandidates = getSequenceCandidates(seq); if (null != sequenceCandidates) { for (int i = sequenceCandidates.size() - 1; i >= 0; i--) { RedeliverCandidate candidate = sequenceCandidates.get(i); long m = candidate.getNumber(); sequenceCandidates.remove(i); candidate.resolved(); undeliveredCount--; purged.add(m); } if (sequenceCandidates.isEmpty()) { candidates.remove(seq.getIdentifier().getValue()); } } LOG.fine("Completed purging redeliver candidates."); } if (!purged.isEmpty()) { RMStore store = manager.getStore(); if (null != store) { store.removeMessages(seq.getIdentifier(), purged, false); } } } public List<Long> getUndeliveredMessageNumbers(DestinationSequence seq) { List<Long> undelivered = new ArrayList<>(); List<RedeliverCandidate> sequenceCandidates = getSequenceCandidates(seq); if (null != sequenceCandidates) { for (int i = 0; i < sequenceCandidates.size(); i++) { RedeliverCandidate candidate = sequenceCandidates.get(i); RMProperties properties = RMContextUtils.retrieveRMProperties(candidate.getMessage(), false); SequenceType st = properties.getSequence(); undelivered.add(st.getMessageNumber()); } } return undelivered; } /** * @param seq the sequence under consideration * @return the list of resend candidates for that sequence * @pre called with mutex held */ protected List<RedeliverCandidate> getSequenceCandidates(DestinationSequence seq) { return getSequenceCandidates(seq.getIdentifier().getValue()); } /** * @param key the sequence identifier under consideration * @return the list of resend candidates for that sequence * @pre called with mutex held */ protected List<RedeliverCandidate> getSequenceCandidates(String key) { List<RedeliverCandidate> sc = candidates.get(key); if (null == sc) { sc = suspendedCandidates.get(key); } return sc; } /** * @param key the sequence identifier under consideration * @return true if the sequence is currently suspended; false otherwise * @pre called with mutex held */ protected boolean isSequenceSuspended(String key) { return suspendedCandidates.containsKey(key); } public RetryStatus getRedeliveryStatus(DestinationSequence seq, long num) { List<RedeliverCandidate> sequenceCandidates = getSequenceCandidates(seq); if (null != sequenceCandidates) { for (int i = 0; i < sequenceCandidates.size(); i++) { RedeliverCandidate candidate = sequenceCandidates.get(i); RMProperties properties = RMContextUtils.retrieveRMProperties(candidate.getMessage(), false); SequenceType st = properties.getSequence(); if (num == st.getMessageNumber()) { return candidate; } } } return null; } public Map<Long, RetryStatus> getRedeliveryStatuses(DestinationSequence seq) { Map<Long, RetryStatus> cp = new HashMap<>(); List<RedeliverCandidate> sequenceCandidates = getSequenceCandidates(seq); if (null != sequenceCandidates) { for (int i = 0; i < sequenceCandidates.size(); i++) { RedeliverCandidate candidate = sequenceCandidates.get(i); RMProperties properties = RMContextUtils.retrieveRMProperties(candidate.getMessage(), false); SequenceType st = properties.getSequence(); cp.put(st.getMessageNumber(), candidate); } } return cp; } public void start() { // TODO Auto-generated method stub } public void stop(DestinationSequence seq) { synchronized (this) { List<RedeliverCandidate> sequenceCandidates = getSequenceCandidates(seq); if (null != sequenceCandidates) { for (int i = sequenceCandidates.size() - 1; i >= 0; i--) { RedeliverCandidate candidate = sequenceCandidates.get(i); candidate.cancel(); } LOG.log(Level.FINE, "Cancelled redeliveriss for sequence {0}.", seq.getIdentifier().getValue()); } } } public void suspend(DestinationSequence seq) { synchronized (this) { String key = seq.getIdentifier().getValue(); List<RedeliverCandidate> sequenceCandidates = candidates.remove(key); if (null != sequenceCandidates) { for (int i = sequenceCandidates.size() - 1; i >= 0; i--) { RedeliverCandidate candidate = sequenceCandidates.get(i); candidate.suspend(); } suspendedCandidates.put(key, sequenceCandidates); LOG.log(Level.FINE, "Suspended redeliveris for sequence {0}.", key); } } } public void resume(DestinationSequence seq) { synchronized (this) { String key = seq.getIdentifier().getValue(); List<RedeliverCandidate> sequenceCandidates = suspendedCandidates.remove(key); if (null != sequenceCandidates) { for (int i = 0; i < sequenceCandidates.size(); i++) { RedeliverCandidate candidate = sequenceCandidates.get(i); candidate.resume(); } candidates.put(key, sequenceCandidates); LOG.log(Level.FINE, "Resumed redeliveries for sequence {0}.", key); } } } /** * Accepts a new resend candidate. * * @param ctx the message context. * @return ResendCandidate */ protected RedeliverCandidate cacheUndelivered(Message message) { RMProperties rmps = RMContextUtils.retrieveRMProperties(message, false); SequenceType st = rmps.getSequence(); Identifier sid = st.getIdentifier(); String key = sid.getValue(); RedeliverCandidate candidate = null; synchronized (this) { List<RedeliverCandidate> sequenceCandidates = getSequenceCandidates(key); if (null == sequenceCandidates) { sequenceCandidates = new ArrayList<>(); candidates.put(key, sequenceCandidates); } candidate = getRedeliverCandidate(st, sequenceCandidates); if (candidate == null) { candidate = new RedeliverCandidate(message); if (isSequenceSuspended(key)) { candidate.suspend(); } sequenceCandidates.add(candidate); undeliveredCount++; } } LOG.fine("Cached undelivered message."); return candidate; } private RedeliverCandidate getRedeliverCandidate(SequenceType st, List<RedeliverCandidate> rcs) { // assume the size of candidates to be relatively small; otherwise we should use message numbers as keys for (RedeliverCandidate rc : rcs) { if (st.getMessageNumber() == rc.getNumber()) { return rc; } } return null; } protected void purgeDelivered(RedeliverCandidate candidate) { RMProperties rmps = RMContextUtils.retrieveRMProperties(candidate.getMessage(), false); SequenceType st = rmps.getSequence(); Identifier sid = st.getIdentifier(); String key = sid.getValue(); synchronized (this) { List<RedeliverCandidate> sequenceCandidates = getSequenceCandidates(key); if (null != sequenceCandidates) { // TODO use a constant op instead of this inefficient linear op sequenceCandidates.remove(candidate); undeliveredCount--; } if (sequenceCandidates.isEmpty()) { candidates.remove(sid.getValue()); } } LOG.fine("Purged delivered message."); } /** * @return a map relating sequence ID to a lists of un-acknowledged messages * for that sequence */ protected Map<String, List<RedeliverCandidate>> getUndelivered() { return candidates; } private static InterceptorChain getRedeliveryInterceptorChain(Message m, String phase) { Exchange exchange = m.getExchange(); Endpoint ep = exchange.getEndpoint(); Bus bus = exchange.getBus(); PhaseManager pm = bus.getExtension(PhaseManager.class); SortedSet<Phase> phases = new TreeSet<Phase>(pm.getInPhases()); for (Iterator<Phase> it = phases.iterator(); it.hasNext();) { Phase p = it.next(); if (phase.equals(p.getName())) { break; } it.remove(); } PhaseInterceptorChain chain = new PhaseInterceptorChain(phases); List<Interceptor<? extends Message>> il = ep.getInInterceptors(); addInterceptors(chain, il); il = ep.getService().getInInterceptors(); addInterceptors(chain, il); il = ep.getBinding().getInInterceptors(); addInterceptors(chain, il); il = bus.getInInterceptors(); addInterceptors(chain, il); if (ep.getService().getDataBinding() instanceof InterceptorProvider) { il = ((InterceptorProvider)ep.getService().getDataBinding()).getInInterceptors(); addInterceptors(chain, il); } return chain; } private static void addInterceptors(PhaseInterceptorChain chain, List<Interceptor<? extends Message>> il) { for (Interceptor<? extends Message> i : il) { final String iname = i.getClass().getSimpleName(); if ("OneWayProcessorInterceptor".equals(iname) || "MAPAggregatorImpl".equals(iname) || "RMInInterceptor".equals(iname)) { continue; } chain.add(i); } } //TODO refactor this class to unify its functionality with that of ResendCandidate protected class RedeliverCandidate implements Runnable, RetryStatus { private Message message; private long number; private Date next; private TimerTask nextTask; private int retries; private int maxRetries; private long nextInterval; private long backoff; private boolean pending; private boolean suspended; protected RedeliverCandidate(Message m) { message = m; if (message instanceof SoapMessage) { // remove old message headers like WSS headers ((SoapMessage)message).getHeaders().clear(); } RetryPolicyType rmrp = null != manager.getDestinationPolicy() ? manager.getDestinationPolicy().getRetryPolicy() : null; long baseRedeliveryInterval = Long.parseLong(DEFAULT_BASE_REDELIVERY_INTERVAL); if (null != rmrp && rmrp.getInterval() > 0L) { baseRedeliveryInterval = rmrp.getInterval(); } backoff = RedeliveryQueue.DEFAULT_EXPONENTIAL_BACKOFF; next = new Date(System.currentTimeMillis() + baseRedeliveryInterval); nextInterval = baseRedeliveryInterval * backoff; maxRetries = null != rmrp ? rmrp.getMaxRetries() : 0; RMProperties rmprops = RMContextUtils.retrieveRMProperties(message, false); if (null != rmprops) { number = rmprops.getSequence().getMessageNumber(); } if (null != manager.getTimer() && maxRetries != 0) { schedule(); } } /** * Initiate redelivery asynchronsly. * */ protected void initiate() { pending = true; Endpoint ep = message.getExchange().get(Endpoint.class); Executor executor = ep.getExecutor(); if (null == executor) { executor = ep.getService().getExecutor(); LOG.log(Level.FINE, "Using service executor {0}", executor.getClass().getName()); } else { LOG.log(Level.FINE, "Using endpoint executor {0}", executor.getClass().getName()); } try { executor.execute(this); } catch (RejectedExecutionException ex) { LOG.log(Level.SEVERE, "RESEND_INITIATION_FAILED_MSG", ex); } } public void run() { try { if (isPending()) { // redeliver redeliver(); purgeDelivered(this); resolved(); } } catch (Exception ex) { LOG.log(Level.WARNING, "redelivery failed", ex); } finally { attempted(); } } private void redeliver() throws Exception { LOG.log(Level.INFO, "Redelivering ... for " + (1 + retries)); String restartingPhase; if (message.getContent(Exception.class) != null) { message.removeContent(Exception.class); message.getExchange().put(Exception.class, null); // clean-up message for redelivery closeStreamResources(); message.removeContent(Node.class); } InputStream is = null; CachedOutputStream cos = (CachedOutputStream)message.get(RMMessageConstants.SAVED_CONTENT); is = cos.getInputStream(); message.setContent(InputStream.class, is); message = message.getExchange().getEndpoint().getBinding().createMessage(message); restartingPhase = Phase.POST_STREAM; // skip some interceptor chain phases for redelivery InterceptorChain chain = getRedeliveryInterceptorChain(message, restartingPhase); ListIterator<Interceptor<? extends Message>> iterator = chain.getIterator(); while (iterator.hasNext()) { Interceptor<? extends Message> incept = iterator.next(); if (incept.getClass().getName().equals(RMCaptureInInterceptor.class.getName())) { chain.remove(incept); } } message.getExchange().setInMessage(message); message.setInterceptorChain(chain); chain.doIntercept(message); Exception ex = message.getContent(Exception.class); if (null != ex) { throw ex; } } public long getNumber() { return number; } public Date getNext() { return next; } public Date getPrevious() { if (retries > 0) { return new Date(next.getTime() - nextInterval / backoff); } return null; } public int getRetries() { return retries; } public int getMaxRetries() { return maxRetries; } public long getNextInterval() { return nextInterval; } public long getBackoff() { return backoff; } public boolean isPending() { return pending; } public boolean isSuspended() { return suspended; } /** * the message has been delivered to the application */ protected synchronized void resolved() { pending = false; next = null; if (null != nextTask) { nextTask.cancel(); } } /** * Cancel further redelivery (although not successfully delivered). */ protected void cancel() { if (null != nextTask) { nextTask.cancel(); closeStreamResources(); releaseSavedMessage(); } } protected void suspend() { suspended = true; pending = false; //TODO release the message and later reload it upon resume //cancel(); if (null != nextTask) { nextTask.cancel(); } } protected void resume() { suspended = false; next = new Date(System.currentTimeMillis()); attempted(); } private void releaseSavedMessage() { CachedOutputStream saved = (CachedOutputStream)message.remove(RMMessageConstants.SAVED_CONTENT); if (saved != null) { saved.releaseTempFileHold(); try { saved.close(); } catch (IOException e) { // ignore } } // Any unclosed resources must be closed to release the temp files. Closeable closeable = (Closeable)message.get(RMMessageConstants.ATTACHMENTS_CLOSEABLE); if (closeable != null) { try { closeable.close(); } catch (IOException e) { // ignore } } } /* * Close all stream-like resources stored in the message */ private void closeStreamResources() { InputStream oin = message.getContent(InputStream.class); if (oin != null) { try { oin.close(); } catch (Exception e) { // ignore } message.removeContent(InputStream.class); } XMLStreamReader oreader = message.getContent(XMLStreamReader.class); if (oreader != null) { try { oreader.close(); } catch (Exception e) { // ignore } message.removeContent(XMLStreamReader.class); } List<?> olist = message.getContent(List.class); if (olist != null && olist.size() == 1) { Object o = olist.get(0); if (o instanceof XMLStreamReader) { oreader = (XMLStreamReader)o; } else if (o instanceof StaxSource) { oreader = ((StaxSource)o).getXMLStreamReader(); } if (oreader != null) { try { oreader.close(); } catch (Exception e) { // ignore } } message.removeContent(List.class); } } /** * @return associated message context */ protected Message getMessage() { return message; } /** * A resend has been attempted. Schedule the next attempt. */ protected synchronized void attempted() { pending = false; retries++; if (null != next && maxRetries != retries) { next = new Date(next.getTime() + nextInterval); nextInterval *= backoff; schedule(); } } protected final synchronized void schedule() { if (null == manager.getTimer()) { return; } class RedeliverTask extends TimerTask { RedeliverCandidate candidate; RedeliverTask(RedeliverCandidate c) { candidate = c; } @Override public void run() { if (!candidate.isPending()) { candidate.initiate(); } } } nextTask = new RedeliverTask(this); try { manager.getTimer().schedule(nextTask, next); } catch (IllegalStateException ex) { LOG.log(Level.WARNING, "SCHEDULE_RESEND_FAILED_MSG", ex); } } } }