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}