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.slf4j.Logger;
032    import org.slf4j.LoggerFactory;
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 Logger LOG = LoggerFactory.getLogger(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            // VERY IMPORTANT! Otherwise, spymemcached optimizes away concurrent gets
068            builder.setShouldOptimize(false);
069            // We never want spymemcached to time out
070            builder.setOpTimeout(9999999);
071            // Retry upon failure
072            builder.setFailureMode(FailureMode.Retry);
073            memcachedConnectionFactory = builder.build();
074        }
075    
076        public KestrelConfiguration getConfiguration() {
077            return configuration;
078        }
079    
080        public void setConfiguration(KestrelConfiguration configuration) {
081            this.configuration = configuration;
082        }
083    
084        @SuppressWarnings("unchecked")
085        protected KestrelEndpoint createEndpoint(String uri, String remaining, Map parameters) throws Exception {
086            // Copy the configuration as each endpoint can override defaults
087            KestrelConfiguration config = getConfiguration().copy();
088    
089            // Parse the URI, expected to be in one of the following formats:
090            // 1. Use the base KestrelConfiguration for host addresses:
091            //      kestrel://queue[?parameters]
092            //      kestrel:///queue[?parameters]
093            // 2. Override the host, but use the default port:
094            //      kestrel://host/queue[?parameters]
095            // 3. Override the host and port:
096            //      kestrel://host:port/queue[?parameters]
097            // 4. Supply a list of host addresses:
098            //      kestrel://host[:port],host[:port]/queue[?parameters]
099            URI u = new URI(uri);
100            String queue;
101            String[] addresses = null;
102            if (u.getPath() == null || "".equals(u.getPath())) {
103                // This would be the case when they haven't specified any explicit
104                // address(es), and the queue ends up in the "authority" portion of
105                // the URI.  For example:
106                //      kestrel://queue[?parameters]
107                queue = u.getAuthority();
108            } else if (u.getAuthority() == null || "".equals(u.getAuthority())) {
109                // The "path" was present without an authority, such as:
110                //      kestrel:///queue[?parameters]
111                queue = u.getPath();
112            } else {
113                // Both "path" and "authority" were present in the URI, which
114                // means both address(es) and the queue were specified, i.e.:
115                //      kestrel://host/queue[?parameters]
116                //      kestrel://host:port/queue[?parameters]
117                //      kestrel://host[:port],host[:port]/queue[?parameters]
118                addresses = u.getAuthority().split(",");
119                queue = u.getPath();
120            }
121    
122            // Trim off any slash(es), i.e. "/queue/" -> "queue"
123            while (queue.startsWith("/")) {
124                queue = queue.substring(1);
125            }
126            while (queue.endsWith("/")) {
127                queue = queue.substring(0, queue.length() - 1);
128            }
129    
130            if ("".equals(queue)) {
131                // This would be the case if the URI didn't include a path, or if
132                // the path was just "/" or something...throw an exception.
133                throw new IllegalArgumentException("Queue not specified in endpoint URI: " + uri);
134            }
135    
136            if (addresses != null && addresses.length > 0) {
137                // Override the addresses on the copied config
138                config.setAddresses(addresses);
139            } else {
140                // Explicit address(es) weren't specified on the URI, which is
141                // no problem...just default the addresses to whatever was set on
142                // the base KestrelConfiguration.  And since we've already copied
143                // the config, there's nothing else we need to do there.  But let's
144                // make sure the addresses field was indeed set on the base config.
145                if (config.getAddresses() == null) {
146                    throw new IllegalArgumentException("Addresses not set in base configuration or endpoint: " + uri);
147                }
148            }
149    
150            LOG.info("Creating endpoint for queue \"" + queue + "\" on " + config.getAddressesAsString() + ", parameters=" + parameters);
151    
152            // Finally, override config with any supplied URI parameters
153            setProperties(config, parameters);
154    
155            // Create the endpoint for the given queue with the config we built
156            return new KestrelEndpoint(uri, this, config, queue);
157        }
158    
159        public MemcachedClient getMemcachedClient(KestrelConfiguration config, String queue) {
160            String key = config.getAddressesAsString() + "/" + queue;
161            MemcachedClient memcachedClient = memcachedClientCache.get(key);
162            if (memcachedClient != null) {
163                return memcachedClient;
164            }
165            synchronized (memcachedClientCache) {
166                if ((memcachedClient = memcachedClientCache.get(key)) == null) {
167                    LOG.info("Creating MemcachedClient for " + key);
168                    try {
169                        memcachedClient = new MemcachedClient(memcachedConnectionFactory, config.getInetSocketAddresses());
170                    } catch (Exception e) {
171                        throw new RuntimeCamelException("Failed to connect to " + key, e);
172                    }
173                    memcachedClientCache.put(key, memcachedClient);
174                }
175            }
176            return memcachedClient;
177        }
178    
179        public void closeMemcachedClient(String key, MemcachedClient memcachedClient) {
180            try {
181                LOG.debug("Closing client connection to {}", key);
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    }