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}