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.camel.component.seda;
018
019import java.util.ArrayList;
020import java.util.HashSet;
021import java.util.List;
022import java.util.Set;
023import java.util.concurrent.BlockingQueue;
024import java.util.concurrent.CopyOnWriteArraySet;
025import java.util.concurrent.ExecutorService;
026
027import org.apache.camel.AsyncEndpoint;
028import org.apache.camel.Component;
029import org.apache.camel.Consumer;
030import org.apache.camel.Exchange;
031import org.apache.camel.MultipleConsumersSupport;
032import org.apache.camel.PollingConsumer;
033import org.apache.camel.Processor;
034import org.apache.camel.Producer;
035import org.apache.camel.WaitForTaskToComplete;
036import org.apache.camel.api.management.ManagedAttribute;
037import org.apache.camel.api.management.ManagedOperation;
038import org.apache.camel.api.management.ManagedResource;
039import org.apache.camel.impl.DefaultEndpoint;
040import org.apache.camel.processor.MulticastProcessor;
041import org.apache.camel.spi.BrowsableEndpoint;
042import org.apache.camel.spi.Metadata;
043import org.apache.camel.spi.UriEndpoint;
044import org.apache.camel.spi.UriParam;
045import org.apache.camel.spi.UriPath;
046import org.apache.camel.util.ServiceHelper;
047import org.apache.camel.util.URISupport;
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050
051/**
052 * The seda component provides asynchronous call to another endpoint from any CamelContext in the same JVM.
053 */
054@ManagedResource(description = "Managed SedaEndpoint")
055@UriEndpoint(firstVersion = "1.1.0", scheme = "seda", title = "SEDA", syntax = "seda:name", consumerClass = SedaConsumer.class, label = "core,endpoint")
056public class SedaEndpoint extends DefaultEndpoint implements AsyncEndpoint, BrowsableEndpoint, MultipleConsumersSupport {
057    private static final Logger LOG = LoggerFactory.getLogger(SedaEndpoint.class);
058    private final Set<SedaProducer> producers = new CopyOnWriteArraySet<>();
059    private final Set<SedaConsumer> consumers = new CopyOnWriteArraySet<>();
060    private volatile MulticastProcessor consumerMulticastProcessor;
061    private volatile boolean multicastStarted;
062    private volatile ExecutorService multicastExecutor;
063
064    @UriPath(description = "Name of queue") @Metadata(required = "true")
065    private String name;
066    @UriParam(label = "advanced", description = "Define the queue instance which will be used by the endpoint")
067    private BlockingQueue queue;
068    @UriParam(defaultValue = "" + Integer.MAX_VALUE)
069    private int size = Integer.MAX_VALUE;
070
071    @UriParam(label = "consumer", defaultValue = "1")
072    private int concurrentConsumers = 1;
073    @UriParam(label = "consumer,advanced", defaultValue = "true")
074    private boolean limitConcurrentConsumers = true;
075    @UriParam(label = "consumer,advanced")
076    private boolean multipleConsumers;
077    @UriParam(label = "consumer,advanced")
078    private boolean purgeWhenStopping;
079    @UriParam(label = "consumer,advanced", defaultValue = "1000")
080    private int pollTimeout = 1000;
081
082    @UriParam(label = "producer", defaultValue = "IfReplyExpected")
083    private WaitForTaskToComplete waitForTaskToComplete = WaitForTaskToComplete.IfReplyExpected;
084    @UriParam(label = "producer", defaultValue = "30000")
085    private long timeout = 30000;
086    @UriParam(label = "producer")
087    private long offerTimeout;
088    @UriParam(label = "producer")
089    private boolean blockWhenFull;
090    @UriParam(label = "producer")
091    private boolean failIfNoConsumers;
092    @UriParam(label = "producer")
093    private boolean discardIfNoConsumers;
094
095    private BlockingQueueFactory<Exchange> queueFactory;
096
097    public SedaEndpoint() {
098        queueFactory = new LinkedBlockingQueueFactory<>();
099    }
100
101    public SedaEndpoint(String endpointUri, Component component, BlockingQueue<Exchange> queue) {
102        this(endpointUri, component, queue, 1);
103    }
104
105    public SedaEndpoint(String endpointUri, Component component, BlockingQueue<Exchange> queue, int concurrentConsumers) {
106        this(endpointUri, component, concurrentConsumers);
107        this.queue = queue;
108        if (queue != null) {
109            this.size = queue.remainingCapacity();
110        }
111        queueFactory = new LinkedBlockingQueueFactory<>();
112        getComponent().registerQueue(this, queue);
113    }
114
115    public SedaEndpoint(String endpointUri, Component component, BlockingQueueFactory<Exchange> queueFactory, int concurrentConsumers) {
116        this(endpointUri, component, concurrentConsumers);
117        this.queueFactory = queueFactory;
118    }
119
120    private SedaEndpoint(String endpointUri, Component component, int concurrentConsumers) {
121        super(endpointUri, component);
122        this.concurrentConsumers = concurrentConsumers;
123    }
124
125    @Override
126    public SedaComponent getComponent() {
127        return (SedaComponent) super.getComponent();
128    }
129
130    public Producer createProducer() throws Exception {
131        return new SedaProducer(this, getWaitForTaskToComplete(), getTimeout(), isBlockWhenFull(), getOfferTimeout());
132    }
133
134    public Consumer createConsumer(Processor processor) throws Exception {
135        if (getComponent() != null) {
136            // all consumers must match having the same multipleConsumers options
137            String key = getComponent().getQueueKey(getEndpointUri());
138            QueueReference ref = getComponent().getQueueReference(key);
139            if (ref != null && ref.getMultipleConsumers() != isMultipleConsumers()) {
140                // there is already a multiple consumers, so make sure they matches
141                throw new IllegalArgumentException("Cannot use existing queue " + key + " as the existing queue multiple consumers "
142                        + ref.getMultipleConsumers() + " does not match given multiple consumers " + multipleConsumers);
143            }
144        }
145
146        Consumer answer = createNewConsumer(processor);
147        configureConsumer(answer);
148        return answer;
149    }
150
151    protected SedaConsumer createNewConsumer(Processor processor) {
152        return new SedaConsumer(this, processor);
153    }
154
155    @Override
156    public PollingConsumer createPollingConsumer() throws Exception {
157        SedaPollingConsumer answer = new SedaPollingConsumer(this);
158        configureConsumer(answer);
159        return answer;
160    }
161
162    public synchronized BlockingQueue<Exchange> getQueue() {
163        if (queue == null) {
164            // prefer to lookup queue from component, so if this endpoint is re-created or re-started
165            // then the existing queue from the component can be used, so new producers and consumers
166            // can use the already existing queue referenced from the component
167            if (getComponent() != null) {
168                // use null to indicate default size (= use what the existing queue has been configured with)
169                Integer size = getSize() == Integer.MAX_VALUE ? null : getSize();
170                QueueReference ref = getComponent().getOrCreateQueue(this, size, isMultipleConsumers(), queueFactory);
171                queue = ref.getQueue();
172                String key = getComponent().getQueueKey(getEndpointUri());
173                LOG.info("Endpoint {} is using shared queue: {} with size: {}", new Object[]{this, key, ref.getSize() !=  null ? ref.getSize() : Integer.MAX_VALUE});
174                // and set the size we are using
175                if (ref.getSize() != null) {
176                    setSize(ref.getSize());
177                }
178            } else {
179                // fallback and create queue (as this endpoint has no component)
180                queue = createQueue();
181                LOG.info("Endpoint {} is using queue: {} with size: {}", new Object[]{this, getEndpointUri(), getSize()});
182            }
183        }
184        return queue;
185    }
186
187    protected BlockingQueue<Exchange> createQueue() {
188        if (size > 0) {
189            return queueFactory.create(size);
190        } else {
191            return queueFactory.create();
192        }
193    }
194
195    /**
196     * Get's the {@link QueueReference} for the this endpoint.
197     * @return the reference, or <tt>null</tt> if no queue reference exists.
198     */
199    public synchronized QueueReference getQueueReference() {
200        String key = getComponent().getQueueKey(getEndpointUri());
201        QueueReference ref = getComponent().getQueueReference(key);
202        return ref;
203    }
204
205    protected synchronized MulticastProcessor getConsumerMulticastProcessor() throws Exception {
206        if (!multicastStarted && consumerMulticastProcessor != null) {
207            // only start it on-demand to avoid starting it during stopping
208            ServiceHelper.startService(consumerMulticastProcessor);
209            multicastStarted = true;
210        }
211        return consumerMulticastProcessor;
212    }
213
214    protected synchronized void updateMulticastProcessor() throws Exception {
215        // only needed if we support multiple consumers
216        if (!isMultipleConsumersSupported()) {
217            return;
218        }
219
220        // stop old before we create a new
221        if (consumerMulticastProcessor != null) {
222            ServiceHelper.stopService(consumerMulticastProcessor);
223            consumerMulticastProcessor = null;
224        }
225
226        int size = getConsumers().size();
227        if (size >= 1) {
228            if (multicastExecutor == null) {
229                // create multicast executor as we need it when we have more than 1 processor
230                multicastExecutor = getCamelContext().getExecutorServiceManager().newDefaultThreadPool(this, URISupport.sanitizeUri(getEndpointUri()) + "(multicast)");
231            }
232            // create list of consumers to multicast to
233            List<Processor> processors = new ArrayList<>(size);
234            for (SedaConsumer consumer : getConsumers()) {
235                processors.add(consumer.getProcessor());
236            }
237            // create multicast processor
238            multicastStarted = false;
239            consumerMulticastProcessor = new MulticastProcessor(getCamelContext(), processors, null,
240                                                                true, multicastExecutor, false, false, false, 
241                                                                0, null, false, false);
242        }
243    }
244
245    /**
246     * Define the queue instance which will be used by the endpoint.
247     * <p/>
248     * This option is only for rare use-cases where you want to use a custom queue instance.
249     */
250    public void setQueue(BlockingQueue<Exchange> queue) {
251        this.queue = queue;
252        this.size = queue.remainingCapacity();
253    }
254
255    @ManagedAttribute(description = "Queue max capacity")
256    public int getSize() {
257        return size;
258    }
259
260    /**
261     * The maximum capacity of the SEDA queue (i.e., the number of messages it can hold).
262     */
263    public void setSize(int size) {
264        this.size = size;
265    }
266
267    @ManagedAttribute(description = "Current queue size")
268    public int getCurrentQueueSize() {
269        return queue.size();
270    }
271
272    /**
273     * Whether a thread that sends messages to a full SEDA queue will block until the queue's capacity is no longer exhausted.
274     * By default, an exception will be thrown stating that the queue is full.
275     * By enabling this option, the calling thread will instead block and wait until the message can be accepted.
276     */
277    public void setBlockWhenFull(boolean blockWhenFull) {
278        this.blockWhenFull = blockWhenFull;
279    }
280
281    @ManagedAttribute(description = "Whether the caller will block sending to a full queue")
282    public boolean isBlockWhenFull() {
283        return blockWhenFull;
284    }
285
286    /**
287     * Number of concurrent threads processing exchanges.
288     */
289    public void setConcurrentConsumers(int concurrentConsumers) {
290        this.concurrentConsumers = concurrentConsumers;
291    }
292
293    @ManagedAttribute(description = "Number of concurrent consumers")
294    public int getConcurrentConsumers() {
295        return concurrentConsumers;
296    }
297
298    @ManagedAttribute
299    public boolean isLimitConcurrentConsumers() {
300        return limitConcurrentConsumers;
301    }
302
303    /**
304     * Whether to limit the number of concurrentConsumers to the maximum of 500.
305     * By default, an exception will be thrown if an endpoint is configured with a greater number. You can disable that check by turning this option off.
306     */
307    public void setLimitConcurrentConsumers(boolean limitConcurrentConsumers) {
308        this.limitConcurrentConsumers = limitConcurrentConsumers;
309    }
310
311    public WaitForTaskToComplete getWaitForTaskToComplete() {
312        return waitForTaskToComplete;
313    }
314
315    /**
316     * Option to specify whether the caller should wait for the async task to complete or not before continuing.
317     * The following three options are supported: Always, Never or IfReplyExpected.
318     * The first two values are self-explanatory.
319     * The last value, IfReplyExpected, will only wait if the message is Request Reply based.
320     * The default option is IfReplyExpected.
321     */
322    public void setWaitForTaskToComplete(WaitForTaskToComplete waitForTaskToComplete) {
323        this.waitForTaskToComplete = waitForTaskToComplete;
324    }
325
326    @ManagedAttribute
327    public long getTimeout() {
328        return timeout;
329    }
330
331    /**
332     * Timeout (in milliseconds) before a SEDA producer will stop waiting for an asynchronous task to complete.
333     * You can disable timeout by using 0 or a negative value.
334     */
335    public void setTimeout(long timeout) {
336        this.timeout = timeout;
337    }
338    
339    @ManagedAttribute
340    public long getOfferTimeout() {
341        return offerTimeout;
342    }
343    
344    /**
345     * offerTimeout (in milliseconds)  can be added to the block case when queue is full.
346     * You can disable timeout by using 0 or a negative value.
347     */
348    public void setOfferTimeout(long offerTimeout) {
349        this.offerTimeout = offerTimeout;
350    }
351
352    @ManagedAttribute
353    public boolean isFailIfNoConsumers() {
354        return failIfNoConsumers;
355    }
356
357    /**
358     * Whether the producer should fail by throwing an exception, when sending to a queue with no active consumers.
359     * <p/>
360     * Only one of the options <tt>discardIfNoConsumers</tt> and <tt>failIfNoConsumers</tt> can be enabled at the same time.
361     */
362    public void setFailIfNoConsumers(boolean failIfNoConsumers) {
363        this.failIfNoConsumers = failIfNoConsumers;
364    }
365
366    @ManagedAttribute
367    public boolean isDiscardIfNoConsumers() {
368        return discardIfNoConsumers;
369    }
370
371    /**
372     * Whether the producer should discard the message (do not add the message to the queue), when sending to a queue with no active consumers.
373     * <p/>
374     * Only one of the options <tt>discardIfNoConsumers</tt> and <tt>failIfNoConsumers</tt> can be enabled at the same time.
375     */
376    public void setDiscardIfNoConsumers(boolean discardIfNoConsumers) {
377        this.discardIfNoConsumers = discardIfNoConsumers;
378    }
379
380    @ManagedAttribute
381    public boolean isMultipleConsumers() {
382        return multipleConsumers;
383    }
384
385    /**
386     * Specifies whether multiple consumers are allowed. If enabled, you can use SEDA for Publish-Subscribe messaging.
387     * That is, you can send a message to the SEDA queue and have each consumer receive a copy of the message.
388     * When enabled, this option should be specified on every consumer endpoint.
389     */
390    public void setMultipleConsumers(boolean multipleConsumers) {
391        this.multipleConsumers = multipleConsumers;
392    }
393
394    @ManagedAttribute
395    public int getPollTimeout() {
396        return pollTimeout;
397    }
398
399    /**
400     * The timeout used when polling. When a timeout occurs, the consumer can check whether it is allowed to continue running.
401     * Setting a lower value allows the consumer to react more quickly upon shutdown.
402     */
403    public void setPollTimeout(int pollTimeout) {
404        this.pollTimeout = pollTimeout;
405    }
406
407    @ManagedAttribute
408    public boolean isPurgeWhenStopping() {
409        return purgeWhenStopping;
410    }
411
412    /**
413     * Whether to purge the task queue when stopping the consumer/route.
414     * This allows to stop faster, as any pending messages on the queue is discarded.
415     */
416    public void setPurgeWhenStopping(boolean purgeWhenStopping) {
417        this.purgeWhenStopping = purgeWhenStopping;
418    }
419
420    public boolean isSingleton() {
421        return true;
422    }
423
424    /**
425     * Returns the current pending exchanges
426     */
427    public List<Exchange> getExchanges() {
428        return new ArrayList<>(getQueue());
429    }
430
431    @ManagedAttribute
432    public boolean isMultipleConsumersSupported() {
433        return isMultipleConsumers();
434    }
435
436    /**
437     * Purges the queue
438     */
439    @ManagedOperation(description = "Purges the seda queue")
440    public void purgeQueue() {
441        LOG.debug("Purging queue with {} exchanges", queue.size());
442        queue.clear();
443    }
444
445    /**
446     * Returns the current active consumers on this endpoint
447     */
448    public Set<SedaConsumer> getConsumers() {
449        return consumers;
450    }
451
452    /**
453     * Returns the current active producers on this endpoint
454     */
455    public Set<SedaProducer> getProducers() {
456        return new HashSet<>(producers);
457    }
458
459    void onStarted(SedaProducer producer) {
460        producers.add(producer);
461    }
462
463    void onStopped(SedaProducer producer) {
464        producers.remove(producer);
465    }
466
467    void onStarted(SedaConsumer consumer) throws Exception {
468        consumers.add(consumer);
469        if (isMultipleConsumers()) {
470            updateMulticastProcessor();
471        }
472    }
473
474    void onStopped(SedaConsumer consumer) throws Exception {
475        consumers.remove(consumer);
476        if (isMultipleConsumers()) {
477            updateMulticastProcessor();
478        }
479    }
480
481    public boolean hasConsumers() {
482        return this.consumers.size() > 0;
483    }
484
485    @Override
486    protected void doStart() throws Exception {
487        super.doStart();
488
489        // force creating queue when starting
490        if (queue == null) {
491            queue = getQueue();
492        }
493
494        // special for unit testing where we can set a system property to make seda poll faster
495        // and therefore also react faster upon shutdown, which makes overall testing faster of the Camel project
496        String override = System.getProperty("CamelSedaPollTimeout", "" + getPollTimeout());
497        setPollTimeout(Integer.valueOf(override));
498    }
499
500    @Override
501    public void stop() throws Exception {
502        if (getConsumers().isEmpty()) {
503            super.stop();
504        } else {
505            LOG.debug("There is still active consumers.");
506        }
507    }
508
509    @Override
510    public void shutdown() throws Exception {
511        if (shutdown.get()) {
512            LOG.trace("Service already shut down");
513            return;
514        }
515
516        // notify component we are shutting down this endpoint
517        if (getComponent() != null) {
518            getComponent().onShutdownEndpoint(this);
519        }
520
521        if (getConsumers().isEmpty()) {
522            super.shutdown();
523        } else {
524            LOG.debug("There is still active consumers.");
525        }
526    }
527
528    @Override
529    protected void doShutdown() throws Exception {
530        // shutdown thread pool if it was in use
531        if (multicastExecutor != null) {
532            getCamelContext().getExecutorServiceManager().shutdownNow(multicastExecutor);
533            multicastExecutor = null;
534        }
535
536        // clear queue, as we are shutdown, so if re-created then the queue must be updated
537        queue = null;
538    }
539
540}