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;
023
024import org.apache.activemq.command.ActiveMQDestination;
025import org.apache.activemq.command.ActiveMQMessage;
026import org.apache.activemq.command.ConsumerControl;
027import org.apache.activemq.command.ConsumerId;
028import org.apache.activemq.command.ConsumerInfo;
029import org.apache.activemq.command.ExceptionResponse;
030import org.apache.activemq.command.LocalTransactionId;
031import org.apache.activemq.command.MessageAck;
032import org.apache.activemq.command.MessageDispatch;
033import org.apache.activemq.command.MessagePull;
034import org.apache.activemq.command.RemoveInfo;
035import org.apache.activemq.command.RemoveSubscriptionInfo;
036import org.apache.activemq.command.Response;
037import org.apache.activemq.command.TransactionId;
038import org.apache.activemq.transport.amqp.AmqpProtocolConverter;
039import org.apache.activemq.transport.amqp.ResponseHandler;
040import org.apache.activemq.transport.amqp.message.ActiveMQJMSVendor;
041import org.apache.activemq.transport.amqp.message.AutoOutboundTransformer;
042import org.apache.activemq.transport.amqp.message.EncodedMessage;
043import org.apache.activemq.transport.amqp.message.OutboundTransformer;
044import org.apache.qpid.proton.amqp.messaging.Accepted;
045import org.apache.qpid.proton.amqp.messaging.Modified;
046import org.apache.qpid.proton.amqp.messaging.Outcome;
047import org.apache.qpid.proton.amqp.messaging.Rejected;
048import org.apache.qpid.proton.amqp.messaging.Released;
049import org.apache.qpid.proton.amqp.transaction.TransactionalState;
050import org.apache.qpid.proton.amqp.transport.AmqpError;
051import org.apache.qpid.proton.amqp.transport.DeliveryState;
052import org.apache.qpid.proton.amqp.transport.ErrorCondition;
053import org.apache.qpid.proton.amqp.transport.SenderSettleMode;
054import org.apache.qpid.proton.engine.Delivery;
055import org.apache.qpid.proton.engine.Sender;
056import org.fusesource.hawtbuf.Buffer;
057import org.slf4j.Logger;
058import org.slf4j.LoggerFactory;
059
060/**
061 * An AmqpSender wraps the AMQP Sender end of a link from the remote peer
062 * which holds the corresponding Receiver which receives messages transfered
063 * across the link from the Broker.
064 *
065 * An AmqpSender is in turn a message consumer subscribed to some destination
066 * on the broker.  As messages are dispatched to this sender that are sent on
067 * to the remote Receiver end of the lin.
068 */
069public class AmqpSender extends AmqpAbstractLink<Sender> {
070
071    private static final Logger LOG = LoggerFactory.getLogger(AmqpSender.class);
072
073    private static final byte[] EMPTY_BYTE_ARRAY = new byte[] {};
074
075    private final OutboundTransformer outboundTransformer = new AutoOutboundTransformer(ActiveMQJMSVendor.INSTANCE);
076    private final AmqpTransferTagGenerator tagCache = new AmqpTransferTagGenerator();
077    private final LinkedList<MessageDispatch> outbound = new LinkedList<MessageDispatch>();
078    private final LinkedList<MessageDispatch> dispatchedInTx = new LinkedList<MessageDispatch>();
079    private final String MESSAGE_FORMAT_KEY = outboundTransformer.getPrefixVendor() + "MESSAGE_FORMAT";
080
081    private final ConsumerInfo consumerInfo;
082    private final boolean presettle;
083
084    private int currentCredit;
085    private boolean draining;
086    private long lastDeliveredSequenceId;
087
088    private Buffer currentBuffer;
089    private Delivery currentDelivery;
090
091    /**
092     * Creates a new AmqpSender instance that manages the given Sender
093     *
094     * @param session
095     *        the AmqpSession object that is the parent of this instance.
096     * @param endpoint
097     *        the AMQP Sender instance that this class manages.
098     * @param consumerInfo
099     *        the ConsumerInfo instance that holds configuration for this sender.
100     */
101    public AmqpSender(AmqpSession session, Sender endpoint, ConsumerInfo consumerInfo) {
102        super(session, endpoint);
103
104        this.currentCredit = endpoint.getRemoteCredit();
105        this.consumerInfo = consumerInfo;
106        this.presettle = getEndpoint().getRemoteSenderSettleMode() == SenderSettleMode.SETTLED;
107    }
108
109    @Override
110    public void open() {
111        if (!isClosed()) {
112            session.registerSender(getConsumerId(), this);
113        }
114
115        super.open();
116    }
117
118    @Override
119    public void detach() {
120        if (!isClosed() && isOpened()) {
121            RemoveInfo removeCommand = new RemoveInfo(getConsumerId());
122            removeCommand.setLastDeliveredSequenceId(lastDeliveredSequenceId);
123            sendToActiveMQ(removeCommand, null);
124
125            session.unregisterSender(getConsumerId());
126        }
127
128        super.detach();
129    }
130
131    @Override
132    public void close() {
133        if (!isClosed() && isOpened()) {
134            RemoveInfo removeCommand = new RemoveInfo(getConsumerId());
135            removeCommand.setLastDeliveredSequenceId(lastDeliveredSequenceId);
136            sendToActiveMQ(removeCommand, null);
137
138            if (consumerInfo.isDurable()) {
139                RemoveSubscriptionInfo rsi = new RemoveSubscriptionInfo();
140                rsi.setConnectionId(session.getConnection().getConnectionId());
141                rsi.setSubscriptionName(getEndpoint().getName());
142                rsi.setClientId(session.getConnection().getClientId());
143
144                sendToActiveMQ(rsi, null);
145            }
146
147            session.unregisterSender(getConsumerId());
148        }
149
150        super.close();
151    }
152
153    @Override
154    public void flow() throws Exception {
155        int updatedCredit = getEndpoint().getCredit();
156
157        LOG.trace("Flow: drain={} credit={}, remoteCredit={}",
158                  getEndpoint().getDrain(), getEndpoint().getCredit(), getEndpoint().getRemoteCredit());
159
160        if (getEndpoint().getDrain() && (updatedCredit != currentCredit || !draining)) {
161            currentCredit = updatedCredit >= 0 ? updatedCredit : 0;
162            draining = true;
163
164            // Revert to a pull consumer.
165            ConsumerControl control = new ConsumerControl();
166            control.setConsumerId(getConsumerId());
167            control.setDestination(getDestination());
168            control.setPrefetch(0);
169            sendToActiveMQ(control, null);
170
171            // Now request dispatch of the drain amount, we request immediate
172            // timeout and an completion message regardless so that we can know
173            // when we should marked the link as drained.
174            MessagePull pullRequest = new MessagePull();
175            pullRequest.setConsumerId(getConsumerId());
176            pullRequest.setDestination(getDestination());
177            pullRequest.setTimeout(-1);
178            pullRequest.setAlwaysSignalDone(true);
179            pullRequest.setQuantity(currentCredit);
180            sendToActiveMQ(pullRequest, null);
181        } else if (updatedCredit != currentCredit) {
182            currentCredit = updatedCredit >= 0 ? updatedCredit : 0;
183            ConsumerControl control = new ConsumerControl();
184            control.setConsumerId(getConsumerId());
185            control.setDestination(getDestination());
186            control.setPrefetch(currentCredit);
187            sendToActiveMQ(control, null);
188        }
189    }
190
191    @Override
192    public void delivery(Delivery delivery) throws Exception {
193        MessageDispatch md = (MessageDispatch) delivery.getContext();
194        DeliveryState state = delivery.getRemoteState();
195
196        if (state instanceof TransactionalState) {
197            TransactionalState txState = (TransactionalState) state;
198            LOG.trace("onDelivery: TX delivery state = {}", state);
199            if (txState.getOutcome() != null) {
200                Outcome outcome = txState.getOutcome();
201                if (outcome instanceof Accepted) {
202                    if (!delivery.remotelySettled()) {
203                        TransactionalState txAccepted = new TransactionalState();
204                        txAccepted.setOutcome(Accepted.getInstance());
205                        txAccepted.setTxnId(((TransactionalState) state).getTxnId());
206
207                        delivery.disposition(txAccepted);
208                    }
209                    settle(delivery, MessageAck.DELIVERED_ACK_TYPE);
210                }
211            }
212        } else {
213            if (state instanceof Accepted) {
214                LOG.trace("onDelivery: accepted state = {}", state);
215                if (!delivery.remotelySettled()) {
216                    delivery.disposition(new Accepted());
217                }
218                settle(delivery, MessageAck.INDIVIDUAL_ACK_TYPE);
219            } else if (state instanceof Rejected) {
220                // re-deliver /w incremented delivery counter.
221                md.setRedeliveryCounter(md.getRedeliveryCounter() + 1);
222                LOG.trace("onDelivery: Rejected state = {}, delivery count now {}", state, md.getRedeliveryCounter());
223                settle(delivery, -1);
224            } else if (state instanceof Released) {
225                LOG.trace("onDelivery: Released state = {}", state);
226                // re-deliver && don't increment the counter.
227                settle(delivery, -1);
228            } else if (state instanceof Modified) {
229                Modified modified = (Modified) state;
230                if (Boolean.TRUE.equals(modified.getDeliveryFailed())) {
231                    // increment delivery counter..
232                    md.setRedeliveryCounter(md.getRedeliveryCounter() + 1);
233                }
234                LOG.trace("onDelivery: Modified state = {}, delivery count now {}", state, md.getRedeliveryCounter());
235                byte ackType = -1;
236                Boolean undeliverableHere = modified.getUndeliverableHere();
237                if (undeliverableHere != null && undeliverableHere) {
238                    // receiver does not want the message..
239                    // perhaps we should DLQ it?
240                    ackType = MessageAck.POSION_ACK_TYPE;
241                }
242                settle(delivery, ackType);
243            }
244        }
245
246        pumpOutbound();
247    }
248
249    @Override
250    public void commit() throws Exception {
251        if (!dispatchedInTx.isEmpty()) {
252            for (MessageDispatch md : dispatchedInTx) {
253                MessageAck pendingTxAck = new MessageAck(md, MessageAck.INDIVIDUAL_ACK_TYPE, 1);
254                pendingTxAck.setFirstMessageId(md.getMessage().getMessageId());
255                pendingTxAck.setTransactionId(md.getMessage().getTransactionId());
256
257                LOG.trace("Sending commit Ack to ActiveMQ: {}", pendingTxAck);
258
259                sendToActiveMQ(pendingTxAck, new ResponseHandler() {
260                    @Override
261                    public void onResponse(AmqpProtocolConverter converter, Response response) throws IOException {
262                        if (response.isException()) {
263                            if (response.isException()) {
264                                Throwable exception = ((ExceptionResponse) response).getException();
265                                exception.printStackTrace();
266                                getEndpoint().close();
267                            }
268                        }
269                        session.pumpProtonToSocket();
270                    }
271                });
272            }
273
274            dispatchedInTx.clear();
275        }
276    }
277
278    @Override
279    public void rollback() throws Exception {
280        synchronized (outbound) {
281
282            LOG.trace("Rolling back {} messages for redelivery. ", dispatchedInTx.size());
283
284            for (MessageDispatch dispatch : dispatchedInTx) {
285                dispatch.setRedeliveryCounter(dispatch.getRedeliveryCounter() + 1);
286                dispatch.getMessage().setTransactionId(null);
287                outbound.addFirst(dispatch);
288            }
289
290            dispatchedInTx.clear();
291        }
292    }
293
294    /**
295     * Event point for incoming message from ActiveMQ on this Sender's
296     * corresponding subscription.
297     *
298     * @param dispatch
299     *        the MessageDispatch to process and send across the link.
300     *
301     * @throws Exception if an error occurs while encoding the message for send.
302     */
303    public void onMessageDispatch(MessageDispatch dispatch) throws Exception {
304        if (!isClosed()) {
305            // Lock to prevent stepping on TX redelivery
306            synchronized (outbound) {
307                outbound.addLast(dispatch);
308            }
309            pumpOutbound();
310            session.pumpProtonToSocket();
311        }
312    }
313
314    /**
315     * Called when the Broker sends a ConsumerControl command to the Consumer that
316     * this sender creates to obtain messages to dispatch via the sender for this
317     * end of the open link.
318     *
319     * @param control
320     *        The ConsumerControl command to process.
321     */
322    public void onConsumerControl(ConsumerControl control) {
323        if (control.isClose()) {
324            close(new ErrorCondition(AmqpError.INTERNAL_ERROR, "Receiver forcably closed"));
325            session.pumpProtonToSocket();
326        }
327    }
328
329    @Override
330    public String toString() {
331        return "AmqpSender {" + getConsumerId() + "}";
332    }
333
334    //----- Property getters and setters -------------------------------------//
335
336    public ConsumerId getConsumerId() {
337        return consumerInfo.getConsumerId();
338    }
339
340    @Override
341    public ActiveMQDestination getDestination() {
342        return consumerInfo.getDestination();
343    }
344
345    @Override
346    public void setDestination(ActiveMQDestination destination) {
347        consumerInfo.setDestination(destination);
348    }
349
350    //----- Internal Implementation ------------------------------------------//
351
352    public void pumpOutbound() throws Exception {
353        while (!isClosed()) {
354            while (currentBuffer != null) {
355                int sent = getEndpoint().send(currentBuffer.data, currentBuffer.offset, currentBuffer.length);
356                if (sent > 0) {
357                    currentBuffer.moveHead(sent);
358                    if (currentBuffer.length == 0) {
359                        if (presettle) {
360                            settle(currentDelivery, MessageAck.INDIVIDUAL_ACK_TYPE);
361                        } else {
362                            getEndpoint().advance();
363                        }
364                        currentBuffer = null;
365                        currentDelivery = null;
366                    }
367                } else {
368                    return;
369                }
370            }
371
372            if (outbound.isEmpty()) {
373                return;
374            }
375
376            final MessageDispatch md = outbound.removeFirst();
377            try {
378
379                ActiveMQMessage temp = null;
380                if (md.getMessage() != null) {
381
382                    // Topics can dispatch the same Message to more than one consumer
383                    // so we must copy to prevent concurrent read / write to the same
384                    // message object.
385                    if (md.getDestination().isTopic()) {
386                        synchronized (md.getMessage()) {
387                            temp = (ActiveMQMessage) md.getMessage().copy();
388                        }
389                    } else {
390                        temp = (ActiveMQMessage) md.getMessage();
391                    }
392
393                    if (!temp.getProperties().containsKey(MESSAGE_FORMAT_KEY)) {
394                        temp.setProperty(MESSAGE_FORMAT_KEY, 0);
395                    }
396                }
397
398                final ActiveMQMessage jms = temp;
399                if (jms == null) {
400                    LOG.trace("Sender:[{}] browse done.", getEndpoint().getName());
401                    // It's the end of browse signal in response to a MessagePull
402                    getEndpoint().drained();
403                    draining = false;
404                    currentCredit = 0;
405                } else {
406                    jms.setRedeliveryCounter(md.getRedeliveryCounter());
407                    jms.setReadOnlyBody(true);
408                    final EncodedMessage amqp = outboundTransformer.transform(jms);
409                    if (amqp != null && amqp.getLength() > 0) {
410                        currentBuffer = new Buffer(amqp.getArray(), amqp.getArrayOffset(), amqp.getLength());
411                        if (presettle) {
412                            currentDelivery = getEndpoint().delivery(EMPTY_BYTE_ARRAY, 0, 0);
413                        } else {
414                            final byte[] tag = tagCache.getNextTag();
415                            currentDelivery = getEndpoint().delivery(tag, 0, tag.length);
416                        }
417                        currentDelivery.setContext(md);
418                    } else {
419                        // TODO: message could not be generated what now?
420                    }
421                }
422            } catch (Exception e) {
423                LOG.warn("Error detected while flushing outbound messages: {}", e.getMessage());
424            }
425        }
426    }
427
428    private void settle(final Delivery delivery, final int ackType) throws Exception {
429        byte[] tag = delivery.getTag();
430        if (tag != null && tag.length > 0 && delivery.remotelySettled()) {
431            tagCache.returnTag(tag);
432        }
433
434        if (ackType == -1) {
435            // we are going to settle, but redeliver.. we we won't yet ack to ActiveMQ
436            delivery.settle();
437            onMessageDispatch((MessageDispatch) delivery.getContext());
438        } else {
439            MessageDispatch md = (MessageDispatch) delivery.getContext();
440            lastDeliveredSequenceId = md.getMessage().getMessageId().getBrokerSequenceId();
441            MessageAck ack = new MessageAck();
442            ack.setConsumerId(getConsumerId());
443            ack.setFirstMessageId(md.getMessage().getMessageId());
444            ack.setLastMessageId(md.getMessage().getMessageId());
445            ack.setMessageCount(1);
446            ack.setAckType((byte) ackType);
447            ack.setDestination(md.getDestination());
448
449            DeliveryState remoteState = delivery.getRemoteState();
450            if (remoteState != null && remoteState instanceof TransactionalState) {
451                TransactionalState txState = (TransactionalState) remoteState;
452                TransactionId txId = new LocalTransactionId(session.getConnection().getConnectionId(), toLong(txState.getTxnId()));
453                ack.setTransactionId(txId);
454
455                // Store the message sent in this TX we might need to re-send on rollback
456                session.enlist(txId);
457                md.getMessage().setTransactionId(txId);
458                dispatchedInTx.addFirst(md);
459            }
460
461            LOG.trace("Sending Ack to ActiveMQ: {}", ack);
462
463            sendToActiveMQ(ack, new ResponseHandler() {
464                @Override
465                public void onResponse(AmqpProtocolConverter converter, Response response) throws IOException {
466                    if (response.isException()) {
467                        if (response.isException()) {
468                            Throwable exception = ((ExceptionResponse) response).getException();
469                            exception.printStackTrace();
470                            getEndpoint().close();
471                        }
472                    } else {
473                        delivery.settle();
474                    }
475                    session.pumpProtonToSocket();
476                }
477            });
478        }
479    }
480}