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.broker.scheduler; 018 019import java.io.IOException; 020import java.util.concurrent.atomic.AtomicBoolean; 021 022import javax.jms.MessageFormatException; 023 024import org.apache.activemq.ScheduledMessage; 025import org.apache.activemq.advisory.AdvisorySupport; 026import org.apache.activemq.broker.Broker; 027import org.apache.activemq.broker.BrokerFilter; 028import org.apache.activemq.broker.BrokerService; 029import org.apache.activemq.broker.Connection; 030import org.apache.activemq.broker.ConnectionContext; 031import org.apache.activemq.broker.Connector; 032import org.apache.activemq.broker.ProducerBrokerExchange; 033import org.apache.activemq.broker.region.ConnectionStatistics; 034import org.apache.activemq.command.ActiveMQDestination; 035import org.apache.activemq.command.Command; 036import org.apache.activemq.command.ConnectionControl; 037import org.apache.activemq.command.ExceptionResponse; 038import org.apache.activemq.command.Message; 039import org.apache.activemq.command.MessageId; 040import org.apache.activemq.command.ProducerId; 041import org.apache.activemq.command.ProducerInfo; 042import org.apache.activemq.command.Response; 043import org.apache.activemq.openwire.OpenWireFormat; 044import org.apache.activemq.security.SecurityContext; 045import org.apache.activemq.state.ProducerState; 046import org.apache.activemq.transaction.Synchronization; 047import org.apache.activemq.usage.JobSchedulerUsage; 048import org.apache.activemq.usage.SystemUsage; 049import org.apache.activemq.util.ByteSequence; 050import org.apache.activemq.util.IdGenerator; 051import org.apache.activemq.util.LongSequenceGenerator; 052import org.apache.activemq.util.TypeConversionSupport; 053import org.apache.activemq.wireformat.WireFormat; 054import org.slf4j.Logger; 055import org.slf4j.LoggerFactory; 056 057public class SchedulerBroker extends BrokerFilter implements JobListener { 058 private static final Logger LOG = LoggerFactory.getLogger(SchedulerBroker.class); 059 private static final IdGenerator ID_GENERATOR = new IdGenerator(); 060 private static final LongSequenceGenerator longGenerator = new LongSequenceGenerator(); 061 /** 062 * The max repeat value allowed to prevent clients from causing DoS issues with huge repeat counts 063 */ 064 private static final int MAX_REPEAT_ALLOWED = 1000; 065 private final LongSequenceGenerator messageIdGenerator = new LongSequenceGenerator(); 066 private final AtomicBoolean started = new AtomicBoolean(); 067 private final WireFormat wireFormat = new OpenWireFormat(); 068 private final ConnectionContext context = new ConnectionContext(); 069 private final ProducerId producerId = new ProducerId(); 070 private final SystemUsage systemUsage; 071 072 private final JobSchedulerStore store; 073 private JobScheduler scheduler; 074 private int maxRepeatAllowed = MAX_REPEAT_ALLOWED; 075 076 public SchedulerBroker(BrokerService brokerService, Broker next, JobSchedulerStore store) throws Exception { 077 super(next); 078 079 this.store = store; 080 this.producerId.setConnectionId(ID_GENERATOR.generateId()); 081 this.context.setSecurityContext(SecurityContext.BROKER_SECURITY_CONTEXT); 082 // we only get response on unexpected error 083 this.context.setConnection(new Connection() { 084 @Override 085 public Connector getConnector() { 086 return null; 087 } 088 089 @Override 090 public void dispatchSync(Command message) { 091 if (message instanceof ExceptionResponse) { 092 LOG.warn("Unexpected response: {}", message); 093 } 094 } 095 096 @Override 097 public void dispatchAsync(Command command) { 098 if (command instanceof ExceptionResponse) { 099 LOG.warn("Unexpected response: {}", command); 100 } 101 } 102 103 @Override 104 public Response service(Command command) { 105 return null; 106 } 107 108 @Override 109 public void serviceException(Throwable error) { 110 LOG.warn("Unexpected exception", error); 111 } 112 113 @Override 114 public boolean isSlow() { 115 return false; 116 } 117 118 @Override 119 public boolean isBlocked() { 120 return false; 121 } 122 123 @Override 124 public boolean isConnected() { 125 return false; 126 } 127 128 @Override 129 public boolean isActive() { 130 return false; 131 } 132 133 @Override 134 public int getDispatchQueueSize() { 135 return 0; 136 } 137 138 @Override 139 public ConnectionStatistics getStatistics() { 140 return null; 141 } 142 143 @Override 144 public boolean isManageable() { 145 return false; 146 } 147 148 @Override 149 public String getRemoteAddress() { 150 return null; 151 } 152 153 @Override 154 public void serviceExceptionAsync(IOException e) { 155 LOG.warn("Unexpected async ioexception", e); 156 } 157 158 @Override 159 public String getConnectionId() { 160 return null; 161 } 162 163 @Override 164 public boolean isNetworkConnection() { 165 return false; 166 } 167 168 @Override 169 public boolean isFaultTolerantConnection() { 170 return false; 171 } 172 173 @Override 174 public void updateClient(ConnectionControl control) {} 175 176 @Override 177 public int getActiveTransactionCount() { 178 return 0; 179 } 180 181 @Override 182 public Long getOldestActiveTransactionDuration() { 183 return null; 184 } 185 186 @Override 187 public void start() throws Exception {} 188 189 @Override 190 public void stop() throws Exception {} 191 }); 192 this.context.setBroker(next); 193 this.systemUsage = brokerService.getSystemUsage(); 194 195 wireFormat.setVersion(brokerService.getStoreOpenWireVersion()); 196 } 197 198 public synchronized JobScheduler getJobScheduler() throws Exception { 199 return new JobSchedulerFacade(this); 200 } 201 202 @Override 203 public void start() throws Exception { 204 this.started.set(true); 205 getInternalScheduler(); 206 super.start(); 207 } 208 209 @Override 210 public void stop() throws Exception { 211 if (this.started.compareAndSet(true, false)) { 212 213 if (this.store != null) { 214 this.store.stop(); 215 } 216 if (this.scheduler != null) { 217 this.scheduler.removeListener(this); 218 this.scheduler = null; 219 } 220 } 221 super.stop(); 222 } 223 224 @Override 225 public void send(ProducerBrokerExchange producerExchange, final Message messageSend) throws Exception { 226 ConnectionContext context = producerExchange.getConnectionContext(); 227 228 final String jobId = (String) messageSend.getProperty(ScheduledMessage.AMQ_SCHEDULED_ID); 229 final Object cronValue = messageSend.getProperty(ScheduledMessage.AMQ_SCHEDULED_CRON); 230 final Object periodValue = messageSend.getProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD); 231 final Object delayValue = messageSend.getProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY); 232 233 String physicalName = messageSend.getDestination().getPhysicalName(); 234 boolean schedularManage = physicalName.regionMatches(true, 0, ScheduledMessage.AMQ_SCHEDULER_MANAGEMENT_DESTINATION, 0, 235 ScheduledMessage.AMQ_SCHEDULER_MANAGEMENT_DESTINATION.length()); 236 237 if (schedularManage == true) { 238 239 JobScheduler scheduler = getInternalScheduler(); 240 ActiveMQDestination replyTo = messageSend.getReplyTo(); 241 242 String action = (String) messageSend.getProperty(ScheduledMessage.AMQ_SCHEDULER_ACTION); 243 244 if (action != null) { 245 246 Object startTime = messageSend.getProperty(ScheduledMessage.AMQ_SCHEDULER_ACTION_START_TIME); 247 Object endTime = messageSend.getProperty(ScheduledMessage.AMQ_SCHEDULER_ACTION_END_TIME); 248 249 if (replyTo != null && action.equals(ScheduledMessage.AMQ_SCHEDULER_ACTION_BROWSE)) { 250 251 if (startTime != null && endTime != null) { 252 253 long start = (Long) TypeConversionSupport.convert(startTime, Long.class); 254 long finish = (Long) TypeConversionSupport.convert(endTime, Long.class); 255 256 for (Job job : scheduler.getAllJobs(start, finish)) { 257 sendScheduledJob(producerExchange.getConnectionContext(), job, replyTo); 258 } 259 } else { 260 for (Job job : scheduler.getAllJobs()) { 261 sendScheduledJob(producerExchange.getConnectionContext(), job, replyTo); 262 } 263 } 264 } 265 if (jobId != null && action.equals(ScheduledMessage.AMQ_SCHEDULER_ACTION_REMOVE)) { 266 scheduler.remove(jobId); 267 } else if (action.equals(ScheduledMessage.AMQ_SCHEDULER_ACTION_REMOVEALL)) { 268 269 if (startTime != null && endTime != null) { 270 271 long start = (Long) TypeConversionSupport.convert(startTime, Long.class); 272 long finish = (Long) TypeConversionSupport.convert(endTime, Long.class); 273 274 scheduler.removeAllJobs(start, finish); 275 } else { 276 scheduler.removeAllJobs(); 277 } 278 } 279 } 280 281 } else if ((cronValue != null || periodValue != null || delayValue != null) && jobId == null) { 282 283 // Check for room in the job scheduler store 284 if (systemUsage.getJobSchedulerUsage() != null) { 285 JobSchedulerUsage usage = systemUsage.getJobSchedulerUsage(); 286 if (usage.isFull()) { 287 final String logMessage = "Job Scheduler Store is Full (" + 288 usage.getPercentUsage() + "% of " + usage.getLimit() + 289 "). Stopping producer (" + messageSend.getProducerId() + 290 ") to prevent flooding of the job scheduler store." + 291 " See http://activemq.apache.org/producer-flow-control.html for more info"; 292 293 long start = System.currentTimeMillis(); 294 long nextWarn = start; 295 while (!usage.waitForSpace(1000)) { 296 if (context.getStopping().get()) { 297 throw new IOException("Connection closed, send aborted."); 298 } 299 300 long now = System.currentTimeMillis(); 301 if (now >= nextWarn) { 302 LOG.info("{}: {} (blocking for: {}s)", usage, logMessage, (now - start) / 1000); 303 nextWarn = now + 30000l; 304 } 305 } 306 } 307 } 308 309 if (context.isInTransaction()) { 310 context.getTransaction().addSynchronization(new Synchronization() { 311 @Override 312 public void afterCommit() throws Exception { 313 doSchedule(messageSend, cronValue, periodValue, delayValue); 314 } 315 }); 316 } else { 317 doSchedule(messageSend, cronValue, periodValue, delayValue); 318 } 319 } else { 320 super.send(producerExchange, messageSend); 321 } 322 } 323 324 private void doSchedule(Message messageSend, Object cronValue, Object periodValue, Object delayValue) throws Exception { 325 long delay = 0; 326 long period = 0; 327 int repeat = 0; 328 String cronEntry = ""; 329 330 // clear transaction context 331 Message msg = messageSend.copy(); 332 msg.setTransactionId(null); 333 org.apache.activemq.util.ByteSequence packet = wireFormat.marshal(msg); 334 if (cronValue != null) { 335 cronEntry = cronValue.toString(); 336 } 337 if (periodValue != null) { 338 period = (Long) TypeConversionSupport.convert(periodValue, Long.class); 339 } 340 if (delayValue != null) { 341 delay = (Long) TypeConversionSupport.convert(delayValue, Long.class); 342 } 343 Object repeatValue = msg.getProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT); 344 if (repeatValue != null) { 345 repeat = (Integer) TypeConversionSupport.convert(repeatValue, Integer.class); 346 if (repeat > maxRepeatAllowed) { 347 throw new MessageFormatException("The scheduled repeat value is too large"); 348 } 349 } 350 351 //job id should be unique for every job (Same format as MessageId) 352 MessageId jobId = new MessageId(messageSend.getMessageId().getProducerId(), longGenerator.getNextSequenceId()); 353 354 getInternalScheduler().schedule(jobId.toString(), 355 new ByteSequence(packet.data, packet.offset, packet.length), cronEntry, delay, period, repeat); 356 } 357 358 @Override 359 public void scheduledJob(String id, ByteSequence job) { 360 org.apache.activemq.util.ByteSequence packet = new org.apache.activemq.util.ByteSequence(job.getData(), job.getOffset(), job.getLength()); 361 try { 362 Message messageSend = (Message) wireFormat.unmarshal(packet); 363 messageSend.setOriginalTransactionId(null); 364 Object repeatValue = messageSend.getProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT); 365 Object cronValue = messageSend.getProperty(ScheduledMessage.AMQ_SCHEDULED_CRON); 366 String cronStr = cronValue != null ? cronValue.toString() : null; 367 int repeat = 0; 368 if (repeatValue != null) { 369 repeat = (Integer) TypeConversionSupport.convert(repeatValue, Integer.class); 370 if (repeat > maxRepeatAllowed) { 371 throw new MessageFormatException("The scheduled repeat value is too large"); 372 } 373 } 374 375 if (repeat != 0 || cronStr != null && cronStr.length() > 0) { 376 // create a unique id - the original message could be sent 377 // lots of times 378 messageSend.setMessageId(new MessageId(producerId, messageIdGenerator.getNextSequenceId())); 379 } 380 381 // Add the jobId as a property 382 messageSend.setProperty("scheduledJobId", id); 383 384 // if this goes across a network - we don't want it rescheduled 385 messageSend.removeProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD); 386 messageSend.removeProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY); 387 messageSend.removeProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT); 388 messageSend.removeProperty(ScheduledMessage.AMQ_SCHEDULED_CRON); 389 390 if (messageSend.getTimestamp() > 0 && messageSend.getExpiration() > 0) { 391 392 long oldExpiration = messageSend.getExpiration(); 393 long newTimeStamp = System.currentTimeMillis(); 394 long timeToLive = 0; 395 long oldTimestamp = messageSend.getTimestamp(); 396 397 if (oldExpiration > 0) { 398 timeToLive = oldExpiration - oldTimestamp; 399 } 400 401 long expiration = timeToLive + newTimeStamp; 402 403 if (expiration > oldExpiration) { 404 if (timeToLive > 0 && expiration > 0) { 405 messageSend.setExpiration(expiration); 406 } 407 messageSend.setTimestamp(newTimeStamp); 408 LOG.debug("Set message {} timestamp from {} to {}", 409 messageSend.getMessageId(), oldTimestamp, newTimeStamp); 410 } 411 } 412 413 // Repackage the message contents prior to send now that all updates are complete. 414 messageSend.beforeMarshall(wireFormat); 415 416 final ProducerBrokerExchange producerExchange = new ProducerBrokerExchange(); 417 producerExchange.setConnectionContext(context); 418 producerExchange.setMutable(true); 419 producerExchange.setProducerState(new ProducerState(new ProducerInfo())); 420 super.send(producerExchange, messageSend); 421 } catch (Exception e) { 422 LOG.error("Failed to send scheduled message {}", id, e); 423 } 424 } 425 426 protected synchronized JobScheduler getInternalScheduler() throws Exception { 427 if (this.started.get()) { 428 if (this.scheduler == null && store != null) { 429 this.scheduler = store.getJobScheduler("JMS"); 430 this.scheduler.addListener(this); 431 this.scheduler.startDispatching(); 432 } 433 return this.scheduler; 434 } 435 return null; 436 } 437 438 protected void sendScheduledJob(ConnectionContext context, Job job, ActiveMQDestination replyTo) throws Exception { 439 440 org.apache.activemq.util.ByteSequence packet = new org.apache.activemq.util.ByteSequence(job.getPayload()); 441 try { 442 Message msg = (Message) this.wireFormat.unmarshal(packet); 443 msg.setOriginalTransactionId(null); 444 msg.setPersistent(false); 445 msg.setType(AdvisorySupport.ADIVSORY_MESSAGE_TYPE); 446 msg.setMessageId(new MessageId(this.producerId, this.messageIdGenerator.getNextSequenceId())); 447 448 // Preserve original destination 449 msg.setOriginalDestination(msg.getDestination()); 450 451 msg.setDestination(replyTo); 452 msg.setResponseRequired(false); 453 msg.setProducerId(this.producerId); 454 455 // Add the jobId as a property 456 msg.setProperty("scheduledJobId", job.getJobId()); 457 458 final boolean originalFlowControl = context.isProducerFlowControl(); 459 final ProducerBrokerExchange producerExchange = new ProducerBrokerExchange(); 460 producerExchange.setConnectionContext(context); 461 producerExchange.setMutable(true); 462 producerExchange.setProducerState(new ProducerState(new ProducerInfo())); 463 try { 464 context.setProducerFlowControl(false); 465 this.next.send(producerExchange, msg); 466 } finally { 467 context.setProducerFlowControl(originalFlowControl); 468 } 469 } catch (Exception e) { 470 LOG.error("Failed to send scheduled message {}", job.getJobId(), e); 471 } 472 } 473 474 public int getMaxRepeatAllowed() { 475 return maxRepeatAllowed; 476 } 477 478 public void setMaxRepeatAllowed(int maxRepeatAllowed) { 479 this.maxRepeatAllowed = maxRepeatAllowed; 480 } 481}