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 }