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.transport.amqp.AmqpProtocolConverter;
038import org.apache.activemq.transport.amqp.ResponseHandler;
039import org.apache.activemq.transport.amqp.message.ActiveMQJMSVendor;
040import org.apache.activemq.transport.amqp.message.AutoOutboundTransformer;
041import org.apache.activemq.transport.amqp.message.EncodedMessage;
042import org.apache.activemq.transport.amqp.message.OutboundTransformer;
043import org.apache.qpid.proton.amqp.messaging.Accepted;
044import org.apache.qpid.proton.amqp.messaging.Modified;
045import org.apache.qpid.proton.amqp.messaging.Outcome;
046import org.apache.qpid.proton.amqp.messaging.Rejected;
047import org.apache.qpid.proton.amqp.messaging.Released;
048import org.apache.qpid.proton.amqp.transaction.TransactionalState;
049import org.apache.qpid.proton.amqp.transport.AmqpError;
050import org.apache.qpid.proton.amqp.transport.DeliveryState;
051import org.apache.qpid.proton.amqp.transport.ErrorCondition;
052import org.apache.qpid.proton.amqp.transport.SenderSettleMode;
053import org.apache.qpid.proton.engine.Delivery;
054import org.apache.qpid.proton.engine.Sender;
055import org.fusesource.hawtbuf.Buffer;
056import org.slf4j.Logger;
057import org.slf4j.LoggerFactory;
058
059/**
060 * An AmqpSender wraps the AMQP Sender end of a link from the remote peer
061 * which holds the corresponding Receiver which receives messages transfered
062 * across the link from the Broker.
063 *
064 * An AmqpSender is in turn a message consumer subscribed to some destination
065 * on the broker.  As messages are dispatched to this sender that are sent on
066 * to the remote Receiver end of the lin.
067 */
068public class AmqpSender extends AmqpAbstractLink<Sender> {
069
070    private static final Logger LOG = LoggerFactory.getLogger(AmqpSender.class);
071
072    private static final byte[] EMPTY_BYTE_ARRAY = new byte[] {};
073
074    private final OutboundTransformer outboundTransformer = new AutoOutboundTransformer(ActiveMQJMSVendor.INSTANCE);
075    private final AmqpTransferTagGenerator tagCache = new AmqpTransferTagGenerator();
076    private final LinkedList<MessageDispatch> outbound = new LinkedList<MessageDispatch>();
077    private final LinkedList<MessageDispatch> dispatchedInTx = new LinkedList<MessageDispatch>();
078    private final String MESSAGE_FORMAT_KEY = outboundTransformer.getPrefixVendor() + "MESSAGE_FORMAT";
079
080    private final ConsumerInfo consumerInfo;
081    private final boolean presettle;
082
083    private int currentCredit;
084    private boolean draining;
085    private long lastDeliveredSequenceId;
086
087    private Buffer currentBuffer;
088    private Delivery currentDelivery;
089
090    /**
091     * Creates a new AmqpSender instance that manages the given Sender
092     *
093     * @param session
094     *        the AmqpSession object that is the parent of this instance.
095     * @param endpoint
096     *        the AMQP Sender instance that this class manages.
097     * @param consumerInfo
098     *        the ConsumerInfo instance that holds configuration for this sender.
099     */
100    public AmqpSender(AmqpSession session, Sender endpoint, ConsumerInfo consumerInfo) {
101        super(session, endpoint);
102
103        this.currentCredit = endpoint.getRemoteCredit();
104        this.consumerInfo = consumerInfo;
105        this.presettle = getEndpoint().getRemoteSenderSettleMode() == SenderSettleMode.SETTLED;
106    }
107
108    @Override
109    public void open() {
110        if (!isClosed()) {
111            session.registerSender(getConsumerId(), this);
112        }
113
114        super.open();
115    }
116
117    @Override
118    public void detach() {
119        if (!isClosed() && isOpened()) {
120            RemoveInfo removeCommand = new RemoveInfo(getConsumerId());
121            removeCommand.setLastDeliveredSequenceId(lastDeliveredSequenceId);
122            sendToActiveMQ(removeCommand, null);
123
124            session.unregisterSender(getConsumerId());
125        }
126
127        super.detach();
128    }
129
130    @Override
131    public void close() {
132        if (!isClosed() && isOpened()) {
133            RemoveInfo removeCommand = new RemoveInfo(getConsumerId());
134            removeCommand.setLastDeliveredSequenceId(lastDeliveredSequenceId);
135            sendToActiveMQ(removeCommand, null);
136
137            if (consumerInfo.isDurable()) {
138                RemoveSubscriptionInfo rsi = new RemoveSubscriptionInfo();
139                rsi.setConnectionId(session.getConnection().getConnectionId());
140                rsi.setSubscriptionName(getEndpoint().getName());
141                rsi.setClientId(session.getConnection().getClientId());
142
143                sendToActiveMQ(rsi, null);
144            }
145
146            session.unregisterSender(getConsumerId());
147        }
148
149        super.close();
150    }
151
152    @Override
153    public void flow() throws Exception {
154        int updatedCredit = getEndpoint().getCredit();
155
156        LOG.trace("Flow: drain={} credit={}, remoteCredit={}",
157                  getEndpoint().getDrain(), getEndpoint().getCredit(), getEndpoint().getRemoteCredit());
158
159        if (getEndpoint().getDrain() && (updatedCredit != currentCredit || !draining)) {
160            currentCredit = updatedCredit >= 0 ? updatedCredit : 0;
161            draining = true;
162
163            // Revert to a pull consumer.
164            ConsumerControl control = new ConsumerControl();
165            control.setConsumerId(getConsumerId());
166            control.setDestination(getDestination());
167            control.setPrefetch(0);
168            sendToActiveMQ(control, null);
169
170            // Now request dispatch of the drain amount, we request immediate
171            // timeout and an completion message regardless so that we can know
172            // when we should marked the link as drained.
173            MessagePull pullRequest = new MessagePull();
174            pullRequest.setConsumerId(getConsumerId());
175            pullRequest.setDestination(getDestination());
176            pullRequest.setTimeout(-1);
177            pullRequest.setAlwaysSignalDone(true);
178            pullRequest.setQuantity(currentCredit);
179            sendToActiveMQ(pullRequest, null);
180        } else if (updatedCredit != currentCredit) {
181            currentCredit = updatedCredit >= 0 ? updatedCredit : 0;
182            ConsumerControl control = new ConsumerControl();
183            control.setConsumerId(getConsumerId());
184            control.setDestination(getDestination());
185            control.setPrefetch(currentCredit);
186            sendToActiveMQ(control, null);
187        }
188    }
189
190    @Override
191    public void delivery(Delivery delivery) throws Exception {
192        MessageDispatch md = (MessageDispatch) delivery.getContext();
193        DeliveryState state = delivery.getRemoteState();
194
195        if (state instanceof TransactionalState) {
196            TransactionalState txState = (TransactionalState) state;
197            LOG.trace("onDelivery: TX delivery state = {}", state);
198            if (txState.getOutcome() != null) {
199                Outcome outcome = txState.getOutcome();
200                if (outcome instanceof Accepted) {
201                    if (!delivery.remotelySettled()) {
202                        TransactionalState txAccepted = new TransactionalState();
203                        txAccepted.setOutcome(Accepted.getInstance());
204                        txAccepted.setTxnId(((TransactionalState) state).getTxnId());
205
206                        delivery.disposition(txAccepted);
207                    }
208                    settle(delivery, MessageAck.DELIVERED_ACK_TYPE);
209                }
210            }
211        } else {
212            if (state instanceof Accepted) {
213                LOG.trace("onDelivery: accepted state = {}", state);
214                if (!delivery.remotelySettled()) {
215                    delivery.disposition(new Accepted());
216                }
217                settle(delivery, MessageAck.INDIVIDUAL_ACK_TYPE);
218            } else if (state instanceof Rejected) {
219                // Rejection is a terminal outcome, we poison the message for dispatch to
220                // the DLQ.  If a custom redelivery policy is used on the broker the message
221                // can still be redelivered based on the configation of that policy.
222                LOG.trace("onDelivery: Rejected state = {}, message poisoned.", state, md.getRedeliveryCounter());
223                settle(delivery, MessageAck.POSION_ACK_TYPE);
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 (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 s = (TransactionalState) remoteState;
452                long txid = toLong(s.getTxnId());
453                LocalTransactionId localTxId = new LocalTransactionId(session.getConnection().getConnectionId(), txid);
454                ack.setTransactionId(localTxId);
455
456                // Store the message sent in this TX we might need to
457                // re-send on rollback
458                md.getMessage().setTransactionId(localTxId);
459                dispatchedInTx.addFirst(md);
460            }
461
462            LOG.trace("Sending Ack to ActiveMQ: {}", ack);
463
464            sendToActiveMQ(ack, new ResponseHandler() {
465                @Override
466                public void onResponse(AmqpProtocolConverter converter, Response response) throws IOException {
467                    if (response.isException()) {
468                        if (response.isException()) {
469                            Throwable exception = ((ExceptionResponse) response).getException();
470                            exception.printStackTrace();
471                            getEndpoint().close();
472                        }
473                    } else {
474                        delivery.settle();
475                    }
476                    session.pumpProtonToSocket();
477                }
478            });
479        }
480    }
481}