/*
* Copyright 2013 Couchbase.
*
* 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.memcached;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import org.couchbase.mock.Info;
import org.couchbase.mock.memcached.protocol.ErrorCode;
/**
* Storage operations representing a single vBucket. This is a replacement for
* the old DataStore class. Specifically, it does not check for vBucket
* ownership - that information is handled at the protocol layer.
*
* @author mnunberg
*/
public class VBucketStore {
public interface ItemAction {
public void onAction(VBucketStore store, Item itm, VBucketCoordinates ms);
}
private volatile long casCounter;
private static final long THIRTY_DAYS = 30 * 24 * 60 * 60;
static final int DEFAULT_EXPIRY_TIME = 15;
static final int MAXIMUM_EXPIRY_TIME = 29;
private final Map<KeySpec, Item> kv = new ConcurrentHashMap<KeySpec, Item>();
private final StorageVBucketCoordinates[] vbCoords;
private final Map<CoordKey, VBucketCoordinates>allCoords = new HashMap<CoordKey, VBucketCoordinates>();
public ItemAction onItemDelete;
public ItemAction onItemMutated;
public VBucketStore(VBucketInfo[] vbi) {
vbCoords = new StorageVBucketCoordinates[vbi.length];
setCurrentCoords(vbi);
}
private void logCoords(int vbid, VBucketCoordinates coords) {
CoordKey key = new CoordKey(vbid, coords.getUuid());
allCoords.put(key, coords);
}
public VBucketCoordinates findCoords(int vbid, long uuid) {
return allCoords.get(new CoordKey(vbid, uuid));
}
private void setCurrentCoords(VBucketInfo[] vbi) {
for (int i = 0; i < vbi.length; i++) {
StorageVBucketCoordinates curCoords = new StorageVBucketCoordinates(vbi[i].getUuid());
vbCoords[i] = curCoords;
logCoords(i, curCoords);
}
}
void updateCoords(VBucketInfo[] vbi) {
for (int i = 0; i < vbCoords.length; i++) {
logCoords(i, vbCoords[i]);
}
synchronized (vbCoords) {
setCurrentCoords(vbi);
}
}
public VBucketCoordinates getCurrentCoords(int vbid) {
return vbCoords[vbid];
}
/**
* Increments the current coordinates for a new mutation.
* @param ks The key spec containing the vBucket ID whose coordinates should be increases
* @return A status object.
*/
private MutationStatus incrCoords(KeySpec ks) {
final StorageVBucketCoordinates curCoord;
synchronized (vbCoords) {
curCoord = vbCoords[ks.vbId];
}
long seq = curCoord.incrSeqno();
long uuid = curCoord.getUuid();
VBucketCoordinates coord = new BasicVBucketCoordinates(uuid, seq);
return new MutationStatus(coord);
}
private Item lookup(KeySpec ks) {
Item ii = kv.get(ks);
if (ii == null) {
return null;
}
long now = new Date().getTime() + Info.getClockOffset() * 1000L;
if (ii.getExpiryTime() == 0 || now < ii.getExpiryTimeInMillis()) {
return ii;
}
MutationStatus ms = incrCoords(ii.getKeySpec());
onItemDelete.onAction(this, ii, ms.getCoords());
kv.remove(ks);
return null;
}
public ErrorCode lock(Item item, int expiry) {
if (item.isLocked()) {
return ErrorCode.ETMPFAIL;
} else {
if (expiry == 0 || expiry > MAXIMUM_EXPIRY_TIME) {
expiry = DEFAULT_EXPIRY_TIME;
}
MutationStatus ms = incrCoords(item.getKeySpec());
item.setLockExpiryTime(expiry);
onItemMutated.onAction(this, item, ms.getCoords());
return ErrorCode.SUCCESS;
}
}
public ErrorCode touch(Item item, int expiry) {
item.setExpiryTime(expiry);
MutationStatus ms = incrCoords(item.getKeySpec());
onItemMutated.onAction(this, item, ms.getCoords());
return ErrorCode.SUCCESS;
}
public MutationStatus add(Item item) {
// I don't give a shit about atomicity right now..
Item old = lookup(item.getKeySpec());
if (old != null || item.getCas() != 0) {
return new MutationStatus(ErrorCode.KEY_EEXISTS);
}
item.setCas(++casCounter);
kv.put(item.getKeySpec(), item);
MutationStatus ms = incrCoords(item.getKeySpec());
onItemMutated.onAction(this, item, ms.getCoords());
return ms;
}
public MutationStatus replace(Item item) {
// I don't give a shit about atomicity right now..
Item old = lookup(item.getKeySpec());
if (old == null) {
return new MutationStatus(ErrorCode.KEY_ENOENT);
}
if (item.getCas() != old.getCas()) {
if (item.getCas() != 0) {
return new MutationStatus(ErrorCode.KEY_EEXISTS);
}
}
if (!old.ensureUnlocked(item.getCas())) {
return new MutationStatus(ErrorCode.KEY_EEXISTS);
}
MutationStatus ms = incrCoords(item.getKeySpec());
item.setCas(++casCounter);
kv.put(item.getKeySpec(), item);
onItemMutated.onAction(this, item, ms.getCoords());
return ms;
}
public MutationStatus set(Item item) {
if (item.getCas() == 0) {
Item old = lookup(item.getKeySpec());
if (old != null && old.isLocked()) {
return new MutationStatus(ErrorCode.KEY_EEXISTS);
}
MutationStatus ms = incrCoords(item.getKeySpec());
item.setCas(++casCounter);
kv.put(item.getKeySpec(), item);
onItemMutated.onAction(this, item, ms.getCoords());
return ms;
} else {
return replace(item);
}
}
public MutationStatus delete(KeySpec ks, long cas) {
// I don't give a shit about atomicity right now..
Item i = lookup(ks);
if (i == null) {
return new MutationStatus(ErrorCode.KEY_ENOENT);
}
if (!i.ensureUnlocked(cas)) {
return new MutationStatus(ErrorCode.ETMPFAIL);
}
if (cas == 0 || cas == i.getCas()) {
MutationStatus ms = incrCoords(i.getKeySpec());
kv.remove(ks);
onItemDelete.onAction(this, i, ms.getCoords());
return ms;
}
return new MutationStatus(ErrorCode.KEY_EEXISTS);
}
private MutationStatus modifyItemValue(Item i, boolean isAppend) {
Item old = lookup(i.getKeySpec());
if (old == null) {
return new MutationStatus(ErrorCode.KEY_ENOENT);
}
if (!old.ensureUnlocked(i.getCas())) {
return new MutationStatus(ErrorCode.KEY_EEXISTS);
}
if (isAppend) {
old.append(i);
} else {
old.prepend(i);
}
MutationStatus ms = incrCoords(old.getKeySpec());
old.setCas(++casCounter);
onItemMutated.onAction(this, old, ms.getCoords());
return ms;
}
public MutationStatus append(Item i) {
return modifyItemValue(i, true);
}
public MutationStatus prepend(Item i) {
return modifyItemValue(i, false);
}
public Item get(KeySpec ks) {
return lookup(ks);
}
public Item getRandom() {
Random r = new Random();
while (!kv.isEmpty()) {
Collection<Item> c = kv.values();
int max = r.nextInt(c.size());
Iterator<Item> iter = c.iterator();
for (int i = 0; i < max - 1; i++) {
if (!iter.hasNext()) {
break;
}
iter.next();
}
if (!iter.hasNext()) {
continue;
}
Item itm = lookup(iter.next().getKeySpec());
if (itm != null) {
return itm;
}
}
return null;
}
private void forceMutation(int vbid, Item itm, VBucketCoordinates coords, boolean isDelete) {
StorageVBucketCoordinates cur;
synchronized (vbCoords) {
cur = vbCoords[vbid];
if (cur.getUuid() != coords.getUuid()) {
cur = vbCoords[vbid] = new StorageVBucketCoordinates(coords);
}
}
cur.seekSeqno(coords.getSeqno());
if (isDelete) {
kv.remove(itm.getKeySpec());
onItemDelete.onAction(this, itm, coords);
} else {
kv.put(itm.getKeySpec(), itm);
onItemMutated.onAction(this, itm, coords);
}
}
/**
* Force a storage of an item to the cache.
*
* This assumes the current object belongs to a replica, as it will blindly
* assume information passed here is authoritative.
*
* @param itm The item to mutate (should be a copy of the original)
* @param coords Coordinate info
*/
void forceStorageMutation(Item itm, VBucketCoordinates coords) {
forceMutation(itm.getKeySpec().vbId, itm, coords, false);
}
/**
* Forces the deletion of an item from the case.
*
* @see #forceStorageMutation(Item, VBucketCoordinates)
* @param itm
* @param coords
*/
void forceDeleteMutation(Item itm, VBucketCoordinates coords) {
forceMutation(itm.getKeySpec().vbId, itm, coords, true);
}
public Map<KeySpec,Item> getMap() {
return kv;
}
/**
* Converts an expiration value to an absolute Unix timestamp.
* @param original The original value passed in from the client. This can
* be a relative or absolute (unix) timestamp
* @return The converted value
*/
public static int convertExpiryTime(int original) {
if (original == 0) {
return original;
} else if (original > THIRTY_DAYS) {
return original + (int)Info.getClockOffset();
}
return (int)((new Date().getTime() / 1000) + original + Info.getClockOffset());
}
}