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}