/**
* Copyright 2011 Couchbase, Inc.
*
* Licensed 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.couchbase.mock;
import org.couchbase.mock.http.BucketAdminServer;
import org.couchbase.mock.http.capi.CAPIServer;
import org.couchbase.mock.memcached.*;
import org.couchbase.mock.memcached.protocol.ErrorCode;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Abstract class for all bucket types in Couchbase.
* @see {@link org.couchbase.mock.CouchbaseBucket}
* @see {@link org.couchbase.mock.MemcachedBucket}
*
* A bucket is instantiated via {@link org.couchbase.mock.CouchbaseMock#createBucket(BucketConfiguration)}. The number
* of servers a bucket has is limited to the amount provided in the {@link org.couchbase.mock.BucketConfiguration#numNodes}
* field.
*
* Nodes can be removed and then re-added, but currently new nodes cannot be added once the bucket has been instantiated.
*/
public abstract class Bucket {
private CAPIServer capiServer = null;
private BucketAdminServer adminServer = null;
public enum BucketType {
MEMCACHED,
COUCHBASE
}
protected final VBucketInfo vbInfo[];
protected final MemcachedServer servers[];
protected final int numVBuckets;
protected final int numReplicas;
protected final String poolName = "default";
protected final String name;
protected final CouchbaseMock cluster;
protected final String password;
protected final ReentrantReadWriteLock configurationRwLock;
private final UUID uuid;
/**
* Returns the vBucket map for the given bucket. This is only relevant for {@link org.couchbase.mock.CouchbaseBucket}
* @return An array of vBucket map structures
*/
public VBucketInfo[] getVBucketInfo() {
return vbInfo;
}
/**
* Get the list of servers allocated for this bucket. This returns both active and inactive servers
* @return an array of servers for this bucket.
*/
public MemcachedServer[] getServers() {
return servers;
}
private Iterator<Item> getMasterItemsIterator(final Storage.StorageType type) {
return new Iterator<Item>() {
private int curIndex = -1;
private Iterator<Item> getNextIterator() {
if (++curIndex == servers.length) {
return null;
}
MemcachedServer s = servers[curIndex];
return s.getStorage().getMasterStore(type).iterator();
}
private Iterator<Item> curIterator = getNextIterator();
@Override
public boolean hasNext() {
while (!curIterator.hasNext()) {
curIterator = getNextIterator();
if (curIterator == null) {
return false;
}
}
return true;
}
@Override
public Item next() {
return curIterator.next();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
/**
* Returns an iterable over all the items in this bucket
* @param type The storage location to fetch from
* @return An iterable which will return all items in the bucket.
*
* Note this currently makes a copy of the items list, making it thread safe. It also means
* that this will potentially return stale data
*/
public Iterable<Item> getMasterItems(final Storage.StorageType type) {
return new Iterable<Item>() {
@Override
public Iterator<Item> iterator() {
return getMasterItemsIterator(type);
}
};
}
/**
* Get the server index for a given key
* @param key The key to look up
* @return an index which can be used in the servers array (via getServers)
*/
public short getVbIndexForKey(String key) {
return -1;
}
public Bucket(CouchbaseMock cluster, BucketConfiguration config) throws IOException {
if (config.numVBuckets < 0) {
throw new IllegalArgumentException("Vbucket count must be > 0");
}
if ( (config.numVBuckets & (config.numVBuckets-1)) != 0 ) {
throw new IllegalArgumentException(
"vBucket count must be a power of 2");
}
this.cluster = cluster;
name = config.name;
numVBuckets = config.numVBuckets;
numReplicas = config.numReplicas;
password = config.password;
vbInfo = new VBucketInfo[numVBuckets];
servers = new MemcachedServer[config.numNodes];
uuid = UUID.randomUUID();
this.configurationRwLock = new ReentrantReadWriteLock();
for (int ii = 0; ii < vbInfo.length; ii++) {
vbInfo[ii] = new VBucketInfo();
}
if (this.getClass() != MemcachedBucket.class && this.getClass() != CouchbaseBucket.class) {
throw new FileNotFoundException("I don't know about this type...");
}
for (int ii = 0; ii < servers.length; ii++) {
servers[ii] = new MemcachedServer(this,
config.hostname,
(config.bucketStartPort == 0 ? 0 : config.bucketStartPort + ii),
vbInfo);
}
rebalance();
}
/**
* Create a bucket.
* @param mock The cluster this bucket is a member of
* @param config The configuration for the bucket
* @return The newly instantiated subclass
* @throws IOException
*/
public static Bucket create(CouchbaseMock mock, BucketConfiguration config) throws IOException {
switch (config.type) {
case MEMCACHED:
return new MemcachedBucket(mock, config);
case COUCHBASE:
return new CouchbaseBucket(mock, config);
default:
throw new FileNotFoundException("I don't know about this type...");
}
}
/**
* Gets the type of the bucket
* @return The type of the bucket
*/
public abstract BucketType getType();
// Used internally by CouchbaseMock
void setCAPIServer(CAPIServer server) {
this.capiServer = server;
}
/**
* Get the {@link org.couchbase.mock.http.capi.CAPIServer} object used for managing views.
* @return The view manager
*/
public CAPIServer getCAPIServer() {
return capiServer;
}
void setAdminServer(BucketAdminServer adminServer) {
this.adminServer = adminServer;
}
/**
* Get the object used for handling configuration changes
* @return The configuration manager
*/
public BucketAdminServer getAdminServer() {
return adminServer;
}
/**
* Gets a map of the current bucket configuration which can be JSON-serialized
* as a valid "Cluster configuration". This method is useful for other configuration handlers
* which wish to embed the current bucket's configuration into a larger structure.
*
* The information returned is equivalent to that returned via the
* {@code /pools/default/buckets/$bucket} endpoint in a Couchbase cluster.
*
* @return A map representing the cluster configuration
*
* Note that to avoid race conditions, invoke {@link #configReadLock()}
* before calling this method, and {@link #configReadUnlock()} after calling
* this method.
*/
public abstract Map<String,Object> getConfigMap();
/**
* Returns configuration information common to both Couchbase and Memcached buckets
* @return The configuration object to be injected
*/
protected Map<String,Object> getCommonConfig() {
Map<String,Object> mm = new HashMap<String, Object>();
mm.put("replicaNumber", numReplicas);
Map<String,Object> ramQuota = new HashMap<String, Object>();
ramQuota.put("rawRAM", 1024 * 1024 * 100);
ramQuota.put("ram", 1024 * 1024 * 100);
mm.put("quota", ramQuota);
return mm;
}
/**
* Convenience method to get the JSON-encoded version of the configuration map.
* @return An encoded JSON String
* @see {@link #getConfigMap()}
*/
public final String getJSON() {
return JsonUtils.encode(getConfigMap());
}
/**
* Lock the current configuration for reading. As long as this lock is held, any
* configuration changes to the bucket (such as failover, removing a node, rebalances,
* etc.) will be blocked. Be sure to call {@link #configReadUnlock()} once the lock
* is no longer required.
*
* This method is most useful to ensure that the bucket state remains the same while
* reading configuration-related properties.
*/
public void configReadLock() {
configurationRwLock.readLock().lock();
}
/**
* Unlock the configuration read lock. This is the exit bracket for {@link #configReadLock()}
*/
public void configReadUnlock() {
configurationRwLock.readLock().unlock();
}
/**
* Convenience method to store an item in a bucket
* @param key The key of the item
* @param value The value of the item
* @return The status of the operation
*/
public abstract ErrorCode storeItem(String key, byte[] value);
/**
* Fail over one of the bucket's nodes
* @param index The index of the node to fail over. This index
* is the index of the node within the {@link #servers}
* (or {@link #getServers()} array; not the logical index
* within the vBucket map!
*
* Note this will also automatically rebalance the cluster
*/
public void failover(int index) {
configurationRwLock.writeLock().lock();
try {
if (index >= 0 && index < servers.length) {
servers[index].shutdown();
}
rebalance();
} finally {
configurationRwLock.writeLock().unlock();
}
}
/**
* Re-Add a previously failed-over node
* @param index The index to restore. This should be an index into the
* {@link #servers} array.
*/
public void respawn(int index) {
configurationRwLock.writeLock().lock();
try {
if (index >= 0 && index < servers.length) {
servers[index].startup();
}
rebalance();
} finally {
configurationRwLock.writeLock().unlock();
}
}
void start() {
for (int ii = 0; ii < servers.length; ii++) {
servers[ii].setName(String.format("%s:MCD[%d]", name, ii));
servers[ii].setDaemon(true);
servers[ii].start();
}
}
void stop() {
for (MemcachedServer t : servers) {
t.interrupt();
do {
try {
t.join();
t = null;
} catch (InterruptedException ex) {
Logger.getLogger(CouchbaseMock.class.getName()).log(Level.SEVERE, null, ex);
t.interrupt();
}
} while (t != null);
}
}
/**
* Gets the list of <b>active</b> nodes within the bucket. An active node is one that
* is not failed over
* @return The list of active nodes in the cluster.
*/
public List<MemcachedServer> activeServers() {
ArrayList<MemcachedServer> active = new ArrayList<MemcachedServer>(servers.length);
for (MemcachedServer server : servers) {
if (server.isActive()) {
active.add(server);
}
}
return active;
}
/**
* Issues a rebalance within the bucket. vBuckets which are mapped to failed-over
* nodes are relocated with their first replica being promoted to active.
*/
final void rebalance() {
// Let's start distribute the vbuckets across the servers
configurationRwLock.writeLock().lock();
try {
List<MemcachedServer> nodes = activeServers();
for (int ii = 0; ii < numVBuckets; ++ii) {
Collections.shuffle(nodes);
vbInfo[ii].setOwner(nodes.get(0));
if (nodes.size() < 2) {
continue;
}
List<MemcachedServer> replicas = nodes.subList(1, nodes.size());
if (replicas.size() > numReplicas) {
replicas = replicas.subList(0, numReplicas);
}
vbInfo[ii].setReplicas(replicas);
}
} finally {
configurationRwLock.writeLock().unlock();
}
}
public void regenCoords() {
for (VBucketInfo cur : vbInfo) {
cur.regenerateUuid();
}
for (MemcachedServer s : servers) {
s.getStorage().updateCoordinateInfo(vbInfo);
}
}
/**
* Get the password for this bucket.
* @return The password
*/
public String getPassword() {
return password;
}
/** Get the name of the bucket */
public String getName() {
return name;
}
/** Gets the UUID for the bucket. This is only used to generate the stats response */
public String getUUID() {
return uuid.toString();
}
/** Gets the parent {@link org.couchbase.mock.CouchbaseMock} object */
public CouchbaseMock getCluster() {
return cluster;
}
}