/* * Copyright 2013 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.memcached; import java.security.AccessControlException; import java.util.*; import org.couchbase.mock.Bucket; /** * Class representing a node's storage. * * This has both a "cache" and a "persistent" store, with the aim to support * full replication and persistence semantics. * * The "cache" store receives items (either via a set or a delete). Once this * is done, the replication and/or persistence hooks are invoked. * * The persistent store keeps a copy of each Item (when persisted), and * each server's "Replica" keeps a new copy of the same Item as well. While this * is probably not the most efficient way to go about things, it is crucial in * order to be able to test these types of semantics. * * @author Mark Nunberg <mnunberg@haskalah.org> */ public class Storage { public enum StorageType { CACHE, DISK } private final VBucketInfo vbInfo[]; private final VBucketStore cacheStore; private final PersistentStorage persistStore; private final MemcachedServer server; private boolean persistEnabled = true; private boolean replicationEnabled = true; private final static VBucketCoordinates EMPTY_COORDS = new BasicVBucketCoordinates(0, 0); private class PersistentStorage { class Slot { long uuid = 0; long seqno = 0; Map<KeySpec, Item> mm = new HashMap<KeySpec, Item>(); } final Slot[] slots; PersistentStorage(int nvb) { slots = new Slot[nvb]; } private Slot updateCommon(KeySpec ks, VBucketCoordinates coords) { Slot slot = slots[ks.vbId]; if (slot == null) { slot = slots[ks.vbId] = new Slot(); } if (coords.getUuid() != 0 && coords.getSeqno() != 0) { slot.uuid = coords.getUuid(); slot.seqno = coords.getSeqno(); } return slot; } public void put(Item item, VBucketCoordinates coords) { updateCommon(item.getKeySpec(), coords).mm.put(item.getKeySpec(), item); } public Item get(KeySpec ks) { Slot ss = slots[ks.vbId]; if (ss != null) { return ss.mm.get(ks); } else { return null; } } public Collection<Item> values() { ArrayList<Item> ret = new ArrayList<Item>(); for (Slot s : slots) { if (s != null) { ret.addAll(s.mm.values()); } } return ret; } public void clear() { for (Slot s : slots) { if (s != null) { s.mm.clear(); } } } public void remove(KeySpec ks, VBucketCoordinates coords) { updateCommon(ks, coords).mm.remove(ks); } VBucketCoordinates getCoords(int vbid) { Slot ss = slots[vbid]; long seqno = 0; long uuid = 0; if (ss != null) { seqno = ss.seqno; uuid = ss.uuid; } return new BasicVBucketCoordinates(uuid, seqno); } public void updateSingleCoords(int vbid, VBucketCoordinates coords) { Slot s = slots[vbid]; if (s != null) { s.uuid = coords.getUuid(); s.seqno = coords.getSeqno(); } } } private class DeleteActionCallback implements VBucketStore.ItemAction { private final Storage storage; public DeleteActionCallback(Storage storage) { this.storage = storage; } @Override public void onAction(VBucketStore cacheStore, Item itm, VBucketCoordinates coords) { if (storage.persistEnabled) { storage.persistDeletedItem(itm.getKeySpec(), coords); } if (storage.replicationEnabled) { storage.replicateDeletedItem(itm.getKeySpec(), coords); } } } private class MutateActionCallback implements VBucketStore.ItemAction { private final Storage storage; public MutateActionCallback(Storage storage) { this.storage = storage; } @Override public void onAction(VBucketStore cacheStore, Item itm, VBucketCoordinates coords) { if (storage.persistEnabled) { storage.persistMutatedItem(itm, coords); } if (storage.replicationEnabled) { storage.replicateMutatedItem(itm, coords); } } } public Storage(VBucketInfo vbi[], MemcachedServer server) { vbInfo = vbi; VBucketStore.ItemAction deleteCallback = new DeleteActionCallback(this); VBucketStore.ItemAction mutateCallback = new MutateActionCallback(this); cacheStore = new VBucketStore(vbi); persistStore = new PersistentStorage(vbi.length); cacheStore.onItemDelete = deleteCallback; cacheStore.onItemMutated = mutateCallback; this.server = server; } public void persistDeletedItem(KeySpec ks, VBucketCoordinates coords) { persistStore.remove(ks, coords); } public void persistMutatedItem(Item itm, VBucketCoordinates coords) { persistStore.put(new Item(itm), coords); } private void replicateMutatedItem(Item itm, VBucketCoordinates coords) { VBucketInfo vbi = vbInfo[itm.getKeySpec().vbId]; if (vbi.getOwner() != server) { return; } for (MemcachedServer replica : vbi.getReplicas()) { Item newItem = new Item(itm); VBucketStore rStore = replica.getStorage().cacheStore; rStore.forceStorageMutation(newItem, coords); } } private void replicateDeletedItem(KeySpec ks, VBucketCoordinates coords) { VBucketInfo vbi = vbInfo[ks.vbId]; if (vbi.getOwner() != server) { return; } Item itm = new Item(ks); for (MemcachedServer replica : vbi.getReplicas()) { VBucketStore rStore = replica.getStorage().cacheStore; PersistentStorage pStore = replica.getStorage().persistStore; rStore.forceDeleteMutation(itm, coords); // Nasty hack needed to retain compat with existing tests which assume that // deletion operations on the mock will silently 'persist' this mutation // on disk. pStore.put(itm, coords); } } public Item getCached(KeySpec ks) { return cacheStore.get(ks); } public Item getPersisted(KeySpec ks) { return persistStore.get(ks); } public void putCached(Item itm) { cacheStore.getMap().put(itm.getKeySpec(), itm); } public void putPersisted(Item itm) { persistStore.put(itm, EMPTY_COORDS); } public void removeCached(KeySpec ks) { cacheStore.getMap().remove(ks); } public void removePersisted(KeySpec ks) { persistStore.remove(ks, EMPTY_COORDS); } public VBucketInfo getVBucketInfo(short vb) { if (vb < 0 || vb > vbInfo.length) { throw new AccessControlException("Invalid vBucket"); } return vbInfo[vb]; } private void verifyOwnership(MemcachedServer server, short vBucketId) { if (server != null && server.getBucket().getType() == Bucket.BucketType.MEMCACHED) { return; } if (vBucketId < 0 || vBucketId > vbInfo.length) { throw new AccessControlException("Invalid vBucket"); } VBucketInfo vbi = vbInfo[vBucketId]; if (server != null && vbi.getOwner() != server) { throw new AccessControlException("Server is not master for this vb"); } } public VBucketStore getCache(MemcachedServer server, short vBucketId) { verifyOwnership(server, vBucketId); return cacheStore; } public VBucketStore getCache(short vBucketId) { verifyOwnership(null, vBucketId); return cacheStore; } public Item getRandomItem() { return cacheStore.getRandom(); } public long getPersistedSeqno(short vBucketId) { return persistStore.getCoords(vBucketId).getSeqno(); } public Iterable<Item> getMasterStore(final StorageType type) { // Create the list now: List<Item> validItems = new ArrayList<Item>(); Collection<Item> inputs; if (type == StorageType.CACHE) { inputs = cacheStore.getMap().values(); } else { inputs = persistStore.values(); } for (Item itm : inputs) { int vbId = itm.getKeySpec().vbId; MemcachedServer owner = vbInfo[vbId].getOwner(); if (owner == server) { validItems.add(itm); } } return validItems; } public void flush() { cacheStore.getMap().clear(); persistStore.clear(); } public void updateCoordinateInfo(VBucketInfo[] vbi) { cacheStore.updateCoords(vbi); for (int i = 0; i < vbi.length; i++) { persistStore.updateSingleCoords(i, cacheStore.getCurrentCoords(i)); } } }