/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.component.kestrel;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import net.spy.memcached.ConnectionFactory;
import net.spy.memcached.ConnectionFactoryBuilder;
import net.spy.memcached.FailureMode;
import net.spy.memcached.MemcachedClient;
import org.apache.camel.CamelContext;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.impl.UriEndpointComponent;
import org.apache.camel.spi.Metadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Camel component which offers queueing over the Memcached protocol
* as supported by Kestrel.
*/
public class KestrelComponent extends UriEndpointComponent {
private static final Logger LOG = LoggerFactory.getLogger(KestrelComponent.class);
private ConnectionFactory memcachedConnectionFactory;
/**
* We cache the memcached clients by queue for reuse
*/
private final Map<String, MemcachedClient> memcachedClientCache = new HashMap<String, MemcachedClient>();
@Metadata(label = "advanced")
private KestrelConfiguration configuration;
public KestrelComponent() {
this(new KestrelConfiguration());
}
public KestrelComponent(KestrelConfiguration configuration) {
super(KestrelEndpoint.class);
this.configuration = configuration;
}
public KestrelComponent(CamelContext context) {
super(context, KestrelEndpoint.class);
configuration = new KestrelConfiguration();
}
@Override
protected void doStart() throws Exception {
super.doStart();
ConnectionFactoryBuilder builder = new ConnectionFactoryBuilder();
// VERY IMPORTANT! Otherwise, spymemcached optimizes away concurrent gets
builder.setShouldOptimize(false);
// We never want spymemcached to time out
builder.setOpTimeout(9999999);
// Retry upon failure
builder.setFailureMode(FailureMode.Retry);
memcachedConnectionFactory = builder.build();
}
public KestrelConfiguration getConfiguration() {
return configuration;
}
/**
* To use a shared configured configuration as base for creating new endpoints.
*/
public void setConfiguration(KestrelConfiguration configuration) {
this.configuration = configuration;
}
protected KestrelEndpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception {
// Copy the configuration as each endpoint can override defaults
KestrelConfiguration config = getConfiguration().copy();
// Parse the URI, expected to be in one of the following formats:
// 1. Use the base KestrelConfiguration for host addresses:
// kestrel://queue[?parameters]
// kestrel:///queue[?parameters]
// 2. Override the host, but use the default port:
// kestrel://host/queue[?parameters]
// 3. Override the host and port:
// kestrel://host:port/queue[?parameters]
// 4. Supply a list of host addresses:
// kestrel://host[:port],host[:port]/queue[?parameters]
URI u = new URI(uri);
String queue;
String[] addresses = null;
if (u.getPath() == null || "".equals(u.getPath())) {
// This would be the case when they haven't specified any explicit
// address(es), and the queue ends up in the "authority" portion of
// the URI. For example:
// kestrel://queue[?parameters]
queue = u.getAuthority();
} else if (u.getAuthority() == null || "".equals(u.getAuthority())) {
// The "path" was present without an authority, such as:
// kestrel:///queue[?parameters]
queue = u.getPath();
} else {
// Both "path" and "authority" were present in the URI, which
// means both address(es) and the queue were specified, i.e.:
// kestrel://host/queue[?parameters]
// kestrel://host:port/queue[?parameters]
// kestrel://host[:port],host[:port]/queue[?parameters]
addresses = u.getAuthority().split(",");
queue = u.getPath();
}
// Trim off any slash(es), i.e. "/queue/" -> "queue"
while (queue.startsWith("/")) {
queue = queue.substring(1);
}
while (queue.endsWith("/")) {
queue = queue.substring(0, queue.length() - 1);
}
if ("".equals(queue)) {
// This would be the case if the URI didn't include a path, or if
// the path was just "/" or something...throw an exception.
throw new IllegalArgumentException("Queue not specified in endpoint URI: " + uri);
}
if (addresses != null && addresses.length > 0) {
// Override the addresses on the copied config
config.setAddresses(addresses);
} else {
// Explicit address(es) weren't specified on the URI, which is
// no problem...just default the addresses to whatever was set on
// the base KestrelConfiguration. And since we've already copied
// the config, there's nothing else we need to do there. But let's
// make sure the addresses field was indeed set on the base config.
if (config.getAddresses() == null) {
throw new IllegalArgumentException("Addresses not set in base configuration or endpoint: " + uri);
}
}
LOG.info("Creating endpoint for queue \"" + queue + "\" on " + config.getAddressesAsString() + ", parameters=" + parameters);
// Finally, override config with any supplied URI parameters
setProperties(config, parameters);
// Create the endpoint for the given queue with the config we built
return new KestrelEndpoint(uri, this, config, queue);
}
public MemcachedClient getMemcachedClient(KestrelConfiguration config, String queue) {
String key = config.getAddressesAsString() + "/" + queue;
MemcachedClient memcachedClient = memcachedClientCache.get(key);
if (memcachedClient != null) {
return memcachedClient;
}
synchronized (memcachedClientCache) {
if ((memcachedClient = memcachedClientCache.get(key)) == null) {
LOG.info("Creating MemcachedClient for " + key);
try {
memcachedClient = new MemcachedClient(memcachedConnectionFactory, config.getInetSocketAddresses());
} catch (Exception e) {
throw new RuntimeCamelException("Failed to connect to " + key, e);
}
memcachedClientCache.put(key, memcachedClient);
}
}
return memcachedClient;
}
public void closeMemcachedClient(String key, MemcachedClient memcachedClient) {
try {
LOG.debug("Closing client connection to {}", key);
memcachedClient.shutdown();
memcachedClientCache.remove(key);
} catch (Exception e) {
LOG.warn("Failed to close client connection to " + key, e);
}
}
@Override
protected synchronized void doStop() throws Exception {
// Use a copy so we can clear the memcached client cache eagerly
Map<String, MemcachedClient> copy;
synchronized (memcachedClientCache) {
copy = new HashMap<String, MemcachedClient>(memcachedClientCache);
memcachedClientCache.clear();
}
for (Map.Entry<String, MemcachedClient> entry : copy.entrySet()) {
closeMemcachedClient(entry.getKey(), entry.getValue());
}
super.doStop();
}
}