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}