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