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}