001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.activemq.transport.amqp.protocol;
018
019import static org.apache.activemq.transport.amqp.AmqpSupport.toLong;
020
021import java.io.IOException;
022import java.util.LinkedList;
023import java.util.concurrent.atomic.AtomicInteger;
024
025import org.apache.activemq.broker.region.AbstractSubscription;
026import org.apache.activemq.command.ActiveMQDestination;
027import org.apache.activemq.command.ActiveMQMessage;
028import org.apache.activemq.command.ConsumerControl;
029import org.apache.activemq.command.ConsumerId;
030import org.apache.activemq.command.ConsumerInfo;
031import org.apache.activemq.command.ExceptionResponse;
032import org.apache.activemq.command.LocalTransactionId;
033import org.apache.activemq.command.MessageAck;
034import org.apache.activemq.command.MessageDispatch;
035import org.apache.activemq.command.MessagePull;
036import org.apache.activemq.command.RemoveInfo;
037import org.apache.activemq.command.RemoveSubscriptionInfo;
038import org.apache.activemq.command.Response;
039import org.apache.activemq.command.TransactionId;
040import org.apache.activemq.transport.amqp.AmqpProtocolConverter;
041import org.apache.activemq.transport.amqp.ResponseHandler;
042import org.apache.activemq.transport.amqp.message.AutoOutboundTransformer;
043import org.apache.activemq.transport.amqp.message.EncodedMessage;
044import org.apache.activemq.transport.amqp.message.OutboundTransformer;
045import org.apache.qpid.proton.amqp.messaging.Accepted;
046import org.apache.qpid.proton.amqp.messaging.Modified;
047import org.apache.qpid.proton.amqp.messaging.Outcome;
048import org.apache.qpid.proton.amqp.messaging.Rejected;
049import org.apache.qpid.proton.amqp.messaging.Released;
050import org.apache.qpid.proton.amqp.transaction.TransactionalState;
051import org.apache.qpid.proton.amqp.transport.AmqpError;
052import org.apache.qpid.proton.amqp.transport.DeliveryState;
053import org.apache.qpid.proton.amqp.transport.ErrorCondition;
054import org.apache.qpid.proton.amqp.transport.ReceiverSettleMode;
055import org.apache.qpid.proton.amqp.transport.SenderSettleMode;
056import org.apache.qpid.proton.engine.Delivery;
057import org.apache.qpid.proton.engine.Link;
058import org.apache.qpid.proton.engine.Sender;
059import org.fusesource.hawtbuf.Buffer;
060import org.slf4j.Logger;
061import org.slf4j.LoggerFactory;
062
063/**
064 * An AmqpSender wraps the AMQP Sender end of a link from the remote peer
065 * which holds the corresponding Receiver which receives messages transfered
066 * across the link from the Broker.
067 *
068 * An AmqpSender is in turn a message consumer subscribed to some destination
069 * on the broker.  As messages are dispatched to this sender that are sent on
070 * to the remote Receiver end of the lin.
071 */
072public class AmqpSender extends AmqpAbstractLink<Sender> {
073
074    private static final Logger LOG = LoggerFactory.getLogger(AmqpSender.class);
075
076    private static final byte[] EMPTY_BYTE_ARRAY = new byte[] {};
077
078    private final OutboundTransformer outboundTransformer = new AutoOutboundTransformer();
079    private final AmqpTransferTagGenerator tagCache = new AmqpTransferTagGenerator();
080    private final LinkedList<MessageDispatch> outbound = new LinkedList<>();
081    private final LinkedList<Delivery> dispatchedInTx = new LinkedList<>();
082
083    private final ConsumerInfo consumerInfo;
084    private AbstractSubscription subscription;
085    private AtomicInteger prefetchExtension;
086    private int currentCreditRequest;
087    private int logicalDeliveryCount; // echoes prefetch extension but from protons perspective
088    private final boolean presettle;
089
090    private boolean draining;
091    private long lastDeliveredSequenceId;
092
093    private Buffer currentBuffer;
094    private Delivery currentDelivery;
095
096    /**
097     * Creates a new AmqpSender instance that manages the given Sender
098     *
099     * @param session
100     *        the AmqpSession object that is the parent of this instance.
101     * @param endpoint
102     *        the AMQP Sender instance that this class manages.
103     * @param consumerInfo
104     *        the ConsumerInfo instance that holds configuration for this sender.
105     */
106    public AmqpSender(AmqpSession session, Sender endpoint, ConsumerInfo consumerInfo) {
107        super(session, endpoint);
108
109        // We don't support second so enforce it as First and let remote decide what to do
110        this.endpoint.setReceiverSettleMode(ReceiverSettleMode.FIRST);
111
112        // Match what the sender mode is
113        this.endpoint.setSenderSettleMode(endpoint.getRemoteSenderSettleMode());
114
115        this.consumerInfo = consumerInfo;
116        this.presettle = getEndpoint().getSenderSettleMode() == SenderSettleMode.SETTLED;
117    }
118
119    @Override
120    public void open() {
121        if (!isClosed()) {
122            session.registerSender(getConsumerId(), this);
123            subscription = (AbstractSubscription)session.getConnection().lookupPrefetchSubscription(consumerInfo);
124            prefetchExtension = subscription.getPrefetchExtension();
125        }
126
127        super.open();
128    }
129
130    @Override
131    public void detach() {
132        if (!isClosed() && isOpened()) {
133            RemoveInfo removeCommand = new RemoveInfo(getConsumerId());
134            removeCommand.setLastDeliveredSequenceId(lastDeliveredSequenceId);
135
136            sendToActiveMQ(removeCommand, new ResponseHandler() {
137
138                @Override
139                public void onResponse(AmqpProtocolConverter converter, Response response) throws IOException {
140                    session.unregisterSender(getConsumerId());
141                    AmqpSender.super.detach();
142                }
143            });
144        } else {
145            super.detach();
146        }
147    }
148
149    @Override
150    public void close() {
151        if (!isClosed() && isOpened()) {
152            RemoveInfo removeCommand = new RemoveInfo(getConsumerId());
153            removeCommand.setLastDeliveredSequenceId(lastDeliveredSequenceId);
154
155            sendToActiveMQ(removeCommand, new ResponseHandler() {
156
157                @Override
158                public void onResponse(AmqpProtocolConverter converter, Response response) throws IOException {
159                    if (consumerInfo.isDurable()) {
160                        RemoveSubscriptionInfo rsi = new RemoveSubscriptionInfo();
161                        rsi.setConnectionId(session.getConnection().getConnectionId());
162                        rsi.setSubscriptionName(getEndpoint().getName());
163                        rsi.setClientId(session.getConnection().getClientId());
164
165                        sendToActiveMQ(rsi);
166                    }
167
168                    session.unregisterSender(getConsumerId());
169                    AmqpSender.super.close();
170                }
171            });
172        } else {
173            super.close();
174        }
175    }
176
177    @Override
178    public void flow() throws Exception {
179        Link endpoint = getEndpoint();
180        if (LOG.isTraceEnabled()) {
181            LOG.trace("Flow: draining={}, drain={} credit={}, currentCredit={}, senderDeliveryCount={} - Sub={}",
182                    draining, endpoint.getDrain(),
183                    endpoint.getCredit(), currentCreditRequest, logicalDeliveryCount, subscription);
184        }
185
186        final int endpointCredit = endpoint.getCredit();
187        if (endpoint.getDrain() && !draining) {
188
189            if (endpointCredit > 0) {
190                draining = true;
191
192                // Now request dispatch of the drain amount, we request immediate
193                // timeout and an completion message regardless so that we can know
194                // when we should marked the link as drained.
195                MessagePull pullRequest = new MessagePull();
196                pullRequest.setConsumerId(getConsumerId());
197                pullRequest.setDestination(getDestination());
198                pullRequest.setTimeout(-1);
199                pullRequest.setAlwaysSignalDone(true);
200                pullRequest.setQuantity(endpointCredit);
201
202                LOG.trace("Pull case -> consumer pull request quantity = {}", endpointCredit);
203
204                sendToActiveMQ(pullRequest);
205            } else {
206                LOG.trace("Pull case -> sending any Queued messages and marking drained");
207
208                pumpOutbound();
209                getEndpoint().drained();
210                session.pumpProtonToSocket();
211                currentCreditRequest = 0;
212                logicalDeliveryCount = 0;
213            }
214        } else if (endpointCredit >= 0) {
215
216            if (endpointCredit == 0 && currentCreditRequest != 0) {
217                prefetchExtension.set(0);
218                currentCreditRequest = 0;
219                logicalDeliveryCount = 0;
220                LOG.trace("Flow: credit 0 for sub:" + subscription);
221            } else {
222                int deltaToAdd = endpointCredit;
223                int logicalCredit = currentCreditRequest - logicalDeliveryCount;
224                if (logicalCredit > 0) {
225                    deltaToAdd -= logicalCredit;
226                } else {
227                    // reset delivery counter - dispatch from broker concurrent with credit=0
228                    // flow can go negative
229                    logicalDeliveryCount = 0;
230                }
231
232                if (deltaToAdd > 0) {
233                    currentCreditRequest = prefetchExtension.addAndGet(deltaToAdd);
234                    subscription.wakeupDestinationsForDispatch();
235                    // force dispatch of matched/pending for topics (pending messages accumulate
236                    // in the sub and are dispatched on update of prefetch)
237                    subscription.setPrefetchSize(0);
238                    LOG.trace("Flow: credit addition of {} for sub {}", deltaToAdd, subscription);
239                }
240            }
241        }
242    }
243
244    @Override
245    public void delivery(Delivery delivery) throws Exception {
246        MessageDispatch md = (MessageDispatch) delivery.getContext();
247        DeliveryState state = delivery.getRemoteState();
248
249        if (state instanceof TransactionalState) {
250            TransactionalState txState = (TransactionalState) state;
251            LOG.trace("onDelivery: TX delivery state = {}", state);
252            if (txState.getOutcome() != null) {
253                Outcome outcome = txState.getOutcome();
254                if (outcome instanceof Accepted) {
255                    TransactionId txId = new LocalTransactionId(session.getConnection().getConnectionId(), toLong(txState.getTxnId()));
256
257                    // Store the message sent in this TX we might need to re-send on rollback
258                    // and we need to ACK it on commit.
259                    session.enlist(txId);
260                    dispatchedInTx.addFirst(delivery);
261
262                    if (!delivery.remotelySettled()) {
263                        TransactionalState txAccepted = new TransactionalState();
264                        txAccepted.setOutcome(Accepted.getInstance());
265                        txAccepted.setTxnId(txState.getTxnId());
266
267                        delivery.disposition(txAccepted);
268                    }
269                }
270            }
271        } else {
272            if (state instanceof Accepted) {
273                LOG.trace("onDelivery: accepted state = {}", state);
274                if (!delivery.remotelySettled()) {
275                    delivery.disposition(new Accepted());
276                }
277                settle(delivery, MessageAck.INDIVIDUAL_ACK_TYPE);
278            } else if (state instanceof Rejected) {
279                // Rejection is a terminal outcome, we poison the message for dispatch to
280                // the DLQ.  If a custom redelivery policy is used on the broker the message
281                // can still be redelivered based on the configation of that policy.
282                LOG.trace("onDelivery: Rejected state = {}, message poisoned.", state);
283                settle(delivery, MessageAck.POSION_ACK_TYPE);
284            } else if (state instanceof Released) {
285                LOG.trace("onDelivery: Released state = {}", state);
286                // re-deliver && don't increment the counter.
287                settle(delivery, -1);
288            } else if (state instanceof Modified) {
289                Modified modified = (Modified) state;
290                if (Boolean.TRUE.equals(modified.getDeliveryFailed())) {
291                    // increment delivery counter..
292                    md.setRedeliveryCounter(md.getRedeliveryCounter() + 1);
293                }
294                LOG.trace("onDelivery: Modified state = {}, delivery count now {}", state, md.getRedeliveryCounter());
295                byte ackType = -1;
296                Boolean undeliverableHere = modified.getUndeliverableHere();
297                if (undeliverableHere != null && undeliverableHere) {
298                    // receiver does not want the message..
299                    // perhaps we should DLQ it?
300                    ackType = MessageAck.POSION_ACK_TYPE;
301                }
302                settle(delivery, ackType);
303            }
304        }
305
306        pumpOutbound();
307    }
308
309    @Override
310    public void commit(LocalTransactionId txnId) throws Exception {
311        if (!dispatchedInTx.isEmpty()) {
312            for (final Delivery delivery : dispatchedInTx) {
313                MessageDispatch dispatch = (MessageDispatch) delivery.getContext();
314
315                MessageAck pendingTxAck = new MessageAck(dispatch, MessageAck.INDIVIDUAL_ACK_TYPE, 1);
316                pendingTxAck.setFirstMessageId(dispatch.getMessage().getMessageId());
317                pendingTxAck.setTransactionId(txnId);
318
319                LOG.trace("Sending commit Ack to ActiveMQ: {}", pendingTxAck);
320
321                sendToActiveMQ(pendingTxAck, new ResponseHandler() {
322                    @Override
323                    public void onResponse(AmqpProtocolConverter converter, Response response) throws IOException {
324                        if (response.isException()) {
325                            Throwable exception = ((ExceptionResponse) response).getException();
326                            exception.printStackTrace();
327                            getEndpoint().close();
328                        } else {
329                            delivery.settle();
330                        }
331                        session.pumpProtonToSocket();
332                    }
333                });
334            }
335
336            dispatchedInTx.clear();
337        }
338    }
339
340    @Override
341    public void rollback(LocalTransactionId txnId) throws Exception {
342        synchronized (outbound) {
343
344            LOG.trace("Rolling back {} messages for redelivery. ", dispatchedInTx.size());
345
346            for (Delivery delivery : dispatchedInTx) {
347                // Only settled deliveries should be re-dispatched, unsettled deliveries
348                // remain acquired on the remote end and can be accepted again in a new
349                // TX or released or rejected etc.
350                MessageDispatch dispatch = (MessageDispatch) delivery.getContext();
351                dispatch.getMessage().setTransactionId(null);
352
353                if (delivery.remotelySettled()) {
354                    dispatch.setRedeliveryCounter(dispatch.getRedeliveryCounter() + 1);
355                    outbound.addFirst(dispatch);
356                }
357            }
358
359            dispatchedInTx.clear();
360        }
361    }
362
363    /**
364     * Event point for incoming message from ActiveMQ on this Sender's
365     * corresponding subscription.
366     *
367     * @param dispatch
368     *        the MessageDispatch to process and send across the link.
369     *
370     * @throws Exception if an error occurs while encoding the message for send.
371     */
372    public void onMessageDispatch(MessageDispatch dispatch) throws Exception {
373        if (!isClosed()) {
374            // Lock to prevent stepping on TX redelivery
375            synchronized (outbound) {
376                outbound.addLast(dispatch);
377            }
378            pumpOutbound();
379            session.pumpProtonToSocket();
380        }
381    }
382
383    /**
384     * Called when the Broker sends a ConsumerControl command to the Consumer that
385     * this sender creates to obtain messages to dispatch via the sender for this
386     * end of the open link.
387     *
388     * @param control
389     *        The ConsumerControl command to process.
390     */
391    public void onConsumerControl(ConsumerControl control) {
392        if (control.isClose()) {
393            close(new ErrorCondition(AmqpError.INTERNAL_ERROR, "Receiver forcably closed"));
394            session.pumpProtonToSocket();
395        }
396    }
397
398    @Override
399    public String toString() {
400        return "AmqpSender {" + getConsumerId() + "}";
401    }
402
403    //----- Property getters and setters -------------------------------------//
404
405    public ConsumerId getConsumerId() {
406        return consumerInfo.getConsumerId();
407    }
408
409    @Override
410    public ActiveMQDestination getDestination() {
411        return consumerInfo.getDestination();
412    }
413
414    @Override
415    public void setDestination(ActiveMQDestination destination) {
416        consumerInfo.setDestination(destination);
417    }
418
419    //----- Internal Implementation ------------------------------------------//
420
421    public void pumpOutbound() throws Exception {
422        while (!isClosed()) {
423            while (currentBuffer != null) {
424                int sent = getEndpoint().send(currentBuffer.data, currentBuffer.offset, currentBuffer.length);
425                if (sent > 0) {
426                    currentBuffer.moveHead(sent);
427                    if (currentBuffer.length == 0) {
428                        if (presettle) {
429                            settle(currentDelivery, MessageAck.INDIVIDUAL_ACK_TYPE);
430                        } else {
431                            getEndpoint().advance();
432                        }
433                        currentBuffer = null;
434                        currentDelivery = null;
435                        logicalDeliveryCount++;
436                    }
437                } else {
438                    return;
439                }
440            }
441
442            if (outbound.isEmpty()) {
443                return;
444            }
445
446            final MessageDispatch md = outbound.removeFirst();
447            try {
448
449                ActiveMQMessage temp = null;
450                if (md.getMessage() != null) {
451                    temp = (ActiveMQMessage) md.getMessage().copy();
452                }
453
454                final ActiveMQMessage jms = temp;
455                if (jms == null) {
456                    LOG.trace("Sender:[{}] browse done.", getEndpoint().getName());
457                    // It's the end of browse signal in response to a MessagePull
458                    getEndpoint().drained();
459                    draining = false;
460                    currentCreditRequest = 0;
461                    logicalDeliveryCount = 0;
462                } else {
463                    if (LOG.isTraceEnabled()) {
464                        LOG.trace("Sender:[{}] msgId={} draining={}, drain={}, credit={}, remoteCredit={}, queued={}",
465                                  getEndpoint().getName(), jms.getJMSMessageID(), draining, getEndpoint().getDrain(),
466                                  getEndpoint().getCredit(), getEndpoint().getRemoteCredit(), getEndpoint().getQueued());
467                    }
468
469                    if (draining && getEndpoint().getCredit() == 0) {
470                        LOG.trace("Sender:[{}] browse complete.", getEndpoint().getName());
471                        getEndpoint().drained();
472                        draining = false;
473                        currentCreditRequest = 0;
474                        logicalDeliveryCount = 0;
475                    }
476
477                    jms.setRedeliveryCounter(md.getRedeliveryCounter());
478                    jms.setReadOnlyBody(true);
479                    final EncodedMessage amqp = outboundTransformer.transform(jms);
480                    if (amqp != null && amqp.getLength() > 0) {
481                        currentBuffer = new Buffer(amqp.getArray(), amqp.getArrayOffset(), amqp.getLength());
482                        if (presettle) {
483                            currentDelivery = getEndpoint().delivery(EMPTY_BYTE_ARRAY, 0, 0);
484                        } else {
485                            final byte[] tag = tagCache.getNextTag();
486                            currentDelivery = getEndpoint().delivery(tag, 0, tag.length);
487                        }
488                        currentDelivery.setContext(md);
489                        currentDelivery.setMessageFormat((int) amqp.getMessageFormat());
490                    } else {
491                        // TODO: message could not be generated what now?
492                    }
493                }
494            } catch (Exception e) {
495                LOG.warn("Error detected while flushing outbound messages: {}", e.getMessage());
496            }
497        }
498    }
499
500    private void settle(final Delivery delivery, final int ackType) throws Exception {
501        byte[] tag = delivery.getTag();
502        if (tag != null && tag.length > 0 && delivery.remotelySettled()) {
503            tagCache.returnTag(tag);
504        }
505
506        if (ackType == -1) {
507            // we are going to settle, but redeliver.. we we won't yet ack to ActiveMQ
508            delivery.settle();
509            onMessageDispatch((MessageDispatch) delivery.getContext());
510        } else {
511            MessageDispatch md = (MessageDispatch) delivery.getContext();
512            lastDeliveredSequenceId = md.getMessage().getMessageId().getBrokerSequenceId();
513            MessageAck ack = new MessageAck();
514            ack.setConsumerId(getConsumerId());
515            ack.setFirstMessageId(md.getMessage().getMessageId());
516            ack.setLastMessageId(md.getMessage().getMessageId());
517            ack.setMessageCount(1);
518            ack.setAckType((byte) ackType);
519            ack.setDestination(md.getDestination());
520            LOG.trace("Sending Ack to ActiveMQ: {}", ack);
521
522            sendToActiveMQ(ack, new ResponseHandler() {
523                @Override
524                public void onResponse(AmqpProtocolConverter converter, Response response) throws IOException {
525                    if (response.isException()) {
526                        if (response.isException()) {
527                            Throwable exception = ((ExceptionResponse) response).getException();
528                            exception.printStackTrace();
529                            getEndpoint().close();
530                        }
531                    } else {
532                        delivery.settle();
533                    }
534                    session.pumpProtonToSocket();
535                }
536            });
537        }
538    }
539}