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     */
017    package org.apache.camel.component.kestrel;
018    
019    import java.net.URI;
020    import java.util.HashMap;
021    import java.util.Map;
022    
023    import net.spy.memcached.ConnectionFactory;
024    import net.spy.memcached.ConnectionFactoryBuilder;
025    import net.spy.memcached.FailureMode;
026    import net.spy.memcached.MemcachedClient;
027    import org.apache.camel.CamelContext;
028    import org.apache.camel.RuntimeCamelException;
029    import org.apache.camel.impl.DefaultComponent;
030    import org.apache.camel.util.ServiceHelper;
031    import org.apache.commons.logging.Log;
032    import org.apache.commons.logging.LogFactory;
033    
034    /**
035     * Camel component which offers queueing over the Memcached protocol
036     * as supported by Kestrel.
037     */
038    public class KestrelComponent extends DefaultComponent {
039        private static final transient Log LOG = LogFactory.getLog(KestrelComponent.class);
040    
041        private KestrelConfiguration configuration;
042        private ConnectionFactory memcachedConnectionFactory;
043    
044        /**
045         * We cache the memcached clients by queue for reuse
046         */
047        private final Map<String, MemcachedClient> memcachedClientCache = new HashMap<String, MemcachedClient>();
048    
049        public KestrelComponent() {
050            configuration = new KestrelConfiguration();
051        }
052    
053        public KestrelComponent(KestrelConfiguration configuration) {
054            this.configuration = configuration;
055        }
056    
057        public KestrelComponent(CamelContext context) {
058            super(context);
059            configuration = new KestrelConfiguration();
060        }
061    
062        @Override
063        protected void doStart() throws Exception {
064            super.doStart();
065    
066            ConnectionFactoryBuilder builder = new ConnectionFactoryBuilder();
067            // We never want spymemcached to time out
068            builder.setOpTimeout(9999999);
069            // Retry upon failure
070            builder.setFailureMode(FailureMode.Retry);
071            memcachedConnectionFactory = builder.build();
072        }
073    
074        public KestrelConfiguration getConfiguration() {
075            return configuration;
076        }
077    
078        public void setConfiguration(KestrelConfiguration configuration) {
079            this.configuration = configuration;
080        }
081    
082        @SuppressWarnings("unchecked")
083        protected KestrelEndpoint createEndpoint(String uri, String remaining, Map parameters) throws Exception {
084            // Copy the configuration as each endpoint can override defaults
085            KestrelConfiguration config = getConfiguration().copy();
086    
087            // Parse the URI, expected to be in one of the following formats:
088            // 1. Use the base KestrelConfiguration for host addresses:
089            //      kestrel://queue[?parameters]
090            //      kestrel:///queue[?parameters]
091            // 2. Override the host, but use the default port:
092            //      kestrel://host/queue[?parameters]
093            // 3. Override the host and port:
094            //      kestrel://host:port/queue[?parameters]
095            // 4. Supply a list of host addresses:
096            //      kestrel://host[:port],host[:port]/queue[?parameters]
097            URI u = new URI(uri);
098            String queue;
099            String[] addresses = null;
100            if (u.getPath() == null || "".equals(u.getPath())) {
101                // This would be the case when they haven't specified any explicit
102                // address(es), and the queue ends up in the "authority" portion of
103                // the URI.  For example:
104                //      kestrel://queue[?parameters]
105                queue = u.getAuthority();
106            } else if (u.getAuthority() == null || "".equals(u.getAuthority())) {
107                // The "path" was present without an authority, such as:
108                //      kestrel:///queue[?parameters]
109                queue = u.getPath();
110            } else {
111                // Both "path" and "authority" were present in the URI, which
112                // means both address(es) and the queue were specified, i.e.:
113                //      kestrel://host/queue[?parameters]
114                //      kestrel://host:port/queue[?parameters]
115                //      kestrel://host[:port],host[:port]/queue[?parameters]
116                addresses = u.getAuthority().split(",");
117                queue = u.getPath();
118            }
119    
120            // Trim off any slash(es), i.e. "/queue/" -> "queue"
121            while (queue.startsWith("/")) {
122                queue = queue.substring(1);
123            }
124            while (queue.endsWith("/")) {
125                queue = queue.substring(0, queue.length() - 1);
126            }
127    
128            if ("".equals(queue)) {
129                // This would be the case if the URI didn't include a path, or if
130                // the path was just "/" or something...throw an exception.
131                throw new IllegalArgumentException("Queue not specified in endpoint URI: " + uri);
132            }
133    
134            if (addresses != null && addresses.length > 0) {
135                // Override the addresses on the copied config
136                config.setAddresses(addresses);
137            } else {
138                // Explicit address(es) weren't specified on the URI, which is
139                // no problem...just default the addresses to whatever was set on
140                // the base KestrelConfiguration.  And since we've already copied
141                // the config, there's nothing else we need to do there.  But let's
142                // make sure the addresses field was indeed set on the base config.
143                if (config.getAddresses() == null) {
144                    throw new IllegalArgumentException("Addresses not set in base configuration or endpoint: " + uri);
145                }
146            }
147    
148            LOG.info("Creating endpoint for queue \"" + queue + "\" on " + config.getAddressesAsString() + ", parameters=" + parameters);
149    
150            // Finally, override config with any supplied URI parameters
151            setProperties(config, parameters);
152    
153            // Create the endpoint for the given queue with the config we built
154            return new KestrelEndpoint(uri, this, config, queue);
155        }
156    
157        public MemcachedClient getMemcachedClient(KestrelConfiguration config, String queue) {
158            String key = config.getAddressesAsString() + "/" + queue;
159            MemcachedClient memcachedClient = memcachedClientCache.get(key);
160            if (memcachedClient != null) {
161                return memcachedClient;
162            }
163            synchronized (memcachedClientCache) {
164                if ((memcachedClient = memcachedClientCache.get(key)) == null) {
165                    LOG.info("Creating MemcachedClient for " + key);
166                    try {
167                        memcachedClient = new MemcachedClient(memcachedConnectionFactory, config.getInetSocketAddresses());
168                    } catch (Exception e) {
169                        throw new RuntimeCamelException("Failed to connect to " + key, e);
170                    }
171                    memcachedClientCache.put(key, memcachedClient);
172                }
173            }
174            return memcachedClient;
175        }
176    
177        public void closeMemcachedClient(String key, MemcachedClient memcachedClient) {
178            try {
179                if (LOG.isDebugEnabled()) {
180                    LOG.debug("Closing client connection to " + key);
181                }
182                memcachedClient.shutdown();
183                memcachedClientCache.remove(key);
184            } catch (Exception e) {
185                LOG.warn("Failed to close client connection to " + key, e);
186            }
187        }
188    
189        @Override
190        protected synchronized void doStop() throws Exception {
191            // Use a copy so we can clear the memcached client cache eagerly
192            Map<String, MemcachedClient> copy;
193            synchronized (memcachedClientCache) {
194                copy = new HashMap<String, MemcachedClient>(memcachedClientCache);
195                memcachedClientCache.clear();
196            }
197    
198            for (Map.Entry<String, MemcachedClient> entry : copy.entrySet()) {
199                closeMemcachedClient(entry.getKey(), entry.getValue());
200            }
201    
202            super.doStop();
203        }
204    }