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 if (LOG.isDebugEnabled()) {
182 LOG.debug("Closing client connection to " + key);
183 }
184 memcachedClient.shutdown();
185 memcachedClientCache.remove(key);
186 } catch (Exception e) {
187 LOG.warn("Failed to close client connection to " + key, e);
188 }
189 }
190
191 @Override
192 protected synchronized void doStop() throws Exception {
193 // Use a copy so we can clear the memcached client cache eagerly
194 Map<String, MemcachedClient> copy;
195 synchronized (memcachedClientCache) {
196 copy = new HashMap<String, MemcachedClient>(memcachedClientCache);
197 memcachedClientCache.clear();
198 }
199
200 for (Map.Entry<String, MemcachedClient> entry : copy.entrySet()) {
201 closeMemcachedClient(entry.getKey(), entry.getValue());
202 }
203
204 super.doStop();
205 }
206 }