All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.appengine.api.memcache.dev.LocalMemcacheService Maven / Gradle / Ivy

Go to download

SDK for dev_appserver (local development) with some of the dependencies shaded (repackaged)

There is a newer version: 2.0.31
Show newest version
/*
 * Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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 com.google.appengine.api.memcache.dev;

import com.google.appengine.api.memcache.MemcacheSerialization;
import com.google.appengine.api.memcache.MemcacheServiceException;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheBatchIncrementRequest;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheBatchIncrementResponse;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheDeleteRequest;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheDeleteResponse;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheDeleteResponse.DeleteStatusCode;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheFlushRequest;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheFlushResponse;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheGetRequest;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheGetResponse;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheIncrementRequest;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheIncrementRequest.Direction;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheIncrementResponse;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheIncrementResponse.IncrementStatusCode;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheServiceError;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheSetRequest;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheSetRequest.SetPolicy;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheSetResponse;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheSetResponse.SetStatusCode;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheStatsRequest;
import com.google.appengine.api.memcache.MemcacheServicePb.MemcacheStatsResponse;
import com.google.appengine.api.memcache.MemcacheServicePb.MergedNamespaceStats;
import com.google.appengine.tools.development.AbstractLocalRpcService;
import com.google.appengine.tools.development.Clock;
import com.google.appengine.tools.development.LatencyPercentiles;
import com.google.appengine.tools.development.LocalRpcService;
import com.google.appengine.tools.development.LocalServiceContext;
import com.google.apphosting.api.ApiProxy;
import com.google.auto.service.AutoService;
import com.google.appengine.repackaged.com.google.protobuf.ByteString;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Java bindings for the local Memcache service. The local cache will by default hold up to 100Mb of
 * combined key-and-value size. That limit can be changed with the init property {@code
 * memcache.maxsize}, set in megabytes ("100M"), in kilobytes ("102400K"), or in bytes
 * ("104857600").
 *
 */
@AutoService(LocalRpcService.class)
public final class LocalMemcacheService extends AbstractLocalRpcService {

  /** The package name for this service. */
  public static final String PACKAGE = "memcache";

  public static final String SIZE_PROPERTY = "memcache.maxsize";
  private static final String DEFAULT_MAX_SIZE = "100M";
  private static final String UTF8 = "UTF-8";
  private static final BigInteger UINT64_MIN_VALUE = BigInteger.ZERO;
  private static final BigInteger UINT64_MAX_VALUE = new BigInteger("FFFFFFFFFFFFFFFF", 16);

  // global unique counter for compare-and-swap operations.
  // This will be globally incremented each time a new CAS ID is assigned, eventually wrapping
  // around.  See //depot/google3/cacheserving/memcacheg/server/item.cc.
  private final AtomicLong globalNextCasId;

  /**
   * A single entry in the cache.
   *
   */
  private class CacheEntry extends LRU.AbstractChainable
      implements Comparable {
    public final String namespace;
    public final Key key;
    public byte[] value;
    int flags;

    /** Expiration in milliseconds-since-epoch */
    public long expires;

    /** Access time in milliseconds-since-epoch */
    public long access;

    public long bytes;

    /** "compare-and-swap" ID. See comments in . */
    public Long casId; // null ==> entry does not have a CAS ID.

    /**
     * Creates a new entry
     *
     * @param namespace the containing namespace
     * @param key the key
     * @param value the value
     * @param flags value-interpretation flags
     * @param expiration expiration in milliseconds-since-epoch
     */
    public CacheEntry(String namespace, Key key, byte[] value, int flags, long expiration) {
      this.namespace = namespace;
      this.key = key;
      this.value = value;
      this.flags = flags;
      this.expires = expiration;
      this.access = clock.getCurrentTime();
      this.bytes = key.getBytes().length + value.length;
      this.casId = null;
    }

    /** Sort cache entries by descending access times. */
    @Override
    public int compareTo(CacheEntry entry) {
      return Long.compare(access, entry.access);
    }

    /**
     * Ensures the CacheEntry has a CAS ID. This CacheEntry will be mutated and assigned a CAS ID if
     * it does not already have one.
     *
     * 

This mutation happens during a set operation that specifies SetPolicy.CAS. */ void markWithCasId() { if (this.hasCasId()) { return; } else { this.casId = globalNextCasId.addAndGet(1) - 1; } } long getCasId() { return casId.longValue(); } boolean hasCasId() { return (casId != null); } } private class LocalStats { private long hits; private long misses; private long hitBytes; private long itemCount; private long totalBytes; private LocalStats(long hits, long misses, long hitBytes, long itemCount, long totalBytes) { this.hits = hits; this.misses = misses; this.hitBytes = hitBytes; this.itemCount = itemCount; this.totalBytes = totalBytes; } public MergedNamespaceStats getAsMergedNamespaceStats() { return MergedNamespaceStats.newBuilder() .setHits(hits) .setMisses(misses) .setByteHits(hitBytes) .setBytes(totalBytes) .setItems(itemCount) .setOldestItemAge(getMaxSecondsWithoutAccess()) .build(); } public int getMaxSecondsWithoutAccess() { if (lru.isEmpty()) { return 0; // no entries } CacheEntry entry = lru.getOldest(); return (int) ((clock.getCurrentTime() - entry.access) / 1000); } public void recordHit(CacheEntry ce) { hits++; hitBytes += ce.bytes; } public void recordMiss() { misses++; } public void recordAdd(CacheEntry ce) { itemCount++; totalBytes += ce.bytes; while (totalBytes > maxSize) { CacheEntry oldest = lru.getOldest(); internalDelete(oldest.namespace, oldest.key); itemCount--; totalBytes -= oldest.bytes; } } public void recordDelete(CacheEntry ce) { itemCount--; totalBytes -= ce.bytes; } } /** * Our keys will be byte[], which by default doesn't do hashCode() and equals() correctly. This * wraps it to do so, using java.util.Arrays. */ private class Key { private byte[] keyval; public Key(byte[] bytes) { keyval = bytes; } public byte[] getBytes() { return keyval; } @Override public boolean equals(Object other) { if (other instanceof Key) { return Arrays.equals(keyval, ((Key) other).keyval); } else { return false; } } @Override public int hashCode() { return Arrays.hashCode(keyval); } } private LRU lru; private final Map> mockCache; private final Map> deleteHold; private long maxSize; private LocalStats stats; private Clock clock; public LocalMemcacheService() { lru = new LRU(); mockCache = new HashMap>(); deleteHold = new HashMap>(); stats = new LocalStats(0, 0, 0, 0, 0); globalNextCasId = new AtomicLong(1); } private Map getOrMakeSubMap(Map> map, K1 key) { Map subMap = map.get(key); if (subMap == null) { subMap = new HashMap(); map.put(key, subMap); } return subMap; } private CacheEntry getWithExpiration(String namespace, Key key) { CacheEntry entry; synchronized (mockCache) { entry = getOrMakeSubMap(mockCache, namespace).get(key); if (entry != null) { if (entry.expires == 0 || clock.getCurrentTime() < entry.expires) { entry.access = clock.getCurrentTime(); lru.update(entry); return entry; } // Clean up expired item. getOrMakeSubMap(mockCache, namespace).remove(key); lru.remove(entry); stats.recordDelete(entry); } } return null; } private CacheEntry internalDelete(String namespace, Key key) { CacheEntry ce; synchronized (mockCache) { ce = getOrMakeSubMap(mockCache, namespace).remove(key); if (ce != null) { lru.remove(ce); } } return ce; } private void internalSet(String namespace, Key key, CacheEntry entry) { synchronized (mockCache) { Map namespaceMap = getOrMakeSubMap(mockCache, namespace); CacheEntry old = namespaceMap.get(key); if (old != null) { // The old entry is no longer valid so remove it from the LRU. The new entry will take its // place when we add it below. lru.remove(old); stats.recordDelete(old); } namespaceMap.put(key, entry); lru.update(entry); stats.recordAdd(entry); } } @Override public String getPackage() { return PACKAGE; } @Override public void init(LocalServiceContext context, Map properties) { this.clock = context.getClock(); String propValue = properties.get(SIZE_PROPERTY); if (propValue == null) { propValue = DEFAULT_MAX_SIZE; } else { propValue = propValue.toUpperCase(); } int multiplier = 1; if (propValue.endsWith("M") || propValue.endsWith("K")) { if (propValue.endsWith("M")) { multiplier = 1024 * 1024; } else { multiplier = 1024; } propValue = propValue.substring(0, propValue.length() - 1); } try { maxSize = Long.parseLong(propValue) * multiplier; } catch (NumberFormatException ex) { throw new MemcacheServiceException( "Can't parse cache size limit '" + properties.get(SIZE_PROPERTY) + "'", ex); } } /** * Skips the system properties to set the limit values for size and element counts. * * @param bytes number of bytes of keys + values that is allowed before old entries are discarded. */ public void setLimits(int bytes) { maxSize = bytes; } @Override public void start() {} @Override public void stop() {} public MemcacheGetResponse get(Status status, MemcacheGetRequest req) { MemcacheGetResponse.Builder result = MemcacheGetResponse.newBuilder(); for (int i = 0; i < req.getKeyCount(); i++) { // our key is always a SHA1 hashcode Key key = new Key(req.getKey(i).toByteArray()); CacheEntry entry = getWithExpiration(req.getNameSpace(), key); if (entry == null) { stats.recordMiss(); } else { stats.recordHit(entry); MemcacheGetResponse.Item.Builder item = MemcacheGetResponse.Item.newBuilder(); item.setKey(ByteString.copyFrom(key.getBytes())) .setFlags(entry.flags) .setValue(ByteString.copyFrom(entry.value)); if (req.hasForCas() && req.getForCas()) { entry.markWithCasId(); item.setCasId(entry.getCasId()); } result.addItem(item.build()); } } status.setSuccessful(true); return result.build(); } public MemcacheSetResponse set(Status status, MemcacheSetRequest req) { MemcacheSetResponse.Builder result = MemcacheSetResponse.newBuilder(); final String namespace = req.getNameSpace(); for (int i = 0; i < req.getItemCount(); i++) { MemcacheSetRequest.Item item = req.getItem(i); Key key = new Key(item.getKey().toByteArray()); SetPolicy policy = item.getSetPolicy(); Map timeoutMap = getOrMakeSubMap(deleteHold, namespace); Long timeout = timeoutMap.get(key); if (timeout != null && policy == SetPolicy.SET) { // A SET operation overrides and clears any timeout that may exist timeout = null; timeoutMap.remove(key); } if ((timeout != null && clock.getCurrentTime() < timeout) || (policy == SetPolicy.CAS && !item.hasCasId())) { result.addSetStatus(SetStatusCode.NOT_STORED); continue; } synchronized (mockCache) { CacheEntry existingEntry = getWithExpiration(namespace, key); if ((policy == SetPolicy.REPLACE && existingEntry == null) || (policy == SetPolicy.ADD && existingEntry != null) || (policy == SetPolicy.CAS && existingEntry == null)) { result.addSetStatus(SetStatusCode.NOT_STORED); } else if (policy == SetPolicy.CAS && (!existingEntry.hasCasId() || existingEntry.getCasId() != item.getCasId())) { result.addSetStatus(SetStatusCode.EXISTS); } else { long expiry = item.hasExpirationTime() ? (long) item.getExpirationTime() : 0L; byte[] value = item.getValue().toByteArray(); int flags = item.getFlags(); // We create a new cacheEntry every time (rather than updating existing ones) to // avoid having to synchronize on reads. (Otherwise a reader on another thread // could be exposed to a partially modified entry). CacheEntry newEntry = new CacheEntry(namespace, key, value, flags, expiry * 1000); internalSet(namespace, key, newEntry); result.addSetStatus(SetStatusCode.STORED); } } } status.setSuccessful(true); return result.build(); } @LatencyPercentiles(latency50th = 4) public MemcacheDeleteResponse delete(Status status, MemcacheDeleteRequest req) { MemcacheDeleteResponse.Builder result = MemcacheDeleteResponse.newBuilder(); final String namespace = req.getNameSpace(); for (int i = 0; i < req.getItemCount(); i++) { MemcacheDeleteRequest.Item item = req.getItem(i); Key key = new Key(item.getKey().toByteArray()); CacheEntry ce = internalDelete(namespace, key); result.addDeleteStatus(ce == null ? DeleteStatusCode.NOT_FOUND : DeleteStatusCode.DELETED); if (ce != null) { stats.recordDelete(ce); } // open spec whether this happens if there was no deletion if (item.hasDeleteTime()) { int millisNoReAdd = item.getDeleteTime() * 1000; getOrMakeSubMap(deleteHold, namespace).put(key, clock.getCurrentTime() + millisNoReAdd); } } status.setSuccessful(true); return result.build(); } public MemcacheIncrementResponse increment(Status status, MemcacheIncrementRequest req) { MemcacheIncrementResponse.Builder result = MemcacheIncrementResponse.newBuilder(); final String namespace = req.getNameSpace(); final Key key = new Key(req.getKey().toByteArray()); final long delta = req.getDirection() == Direction.DECREMENT ? -req.getDelta() : req.getDelta(); synchronized (mockCache) { // only increment offers atomicity CacheEntry ce = getWithExpiration(namespace, key); if (ce == null) { if (req.hasInitialValue()) { // initial value is considered as uint64 and therefore can never be negative BigInteger value = BigInteger.valueOf(req.getInitialValue()).and(UINT64_MAX_VALUE); int flags = req.hasInitialFlags() ? req.getInitialFlags() : MemcacheSerialization.Flag.LONG.ordinal(); ce = new CacheEntry(namespace, key, value.toString().getBytes(), flags, 0); internalSet(namespace, key, ce); } else { stats.recordMiss(); return result.build(); // with hasNewValue() == false } } stats.recordHit(ce); BigInteger value; try { value = new BigInteger(new String(ce.value, UTF8)); } catch (NumberFormatException e) { status.setSuccessful(false); throw new ApiProxy.ApplicationException( MemcacheServiceError.ErrorCode.INVALID_VALUE_VALUE, "Format error"); } catch (UnsupportedEncodingException e) { throw new ApiProxy.UnknownException(UTF8 + " encoding was not found."); } if (value.compareTo(UINT64_MAX_VALUE) > 0 || value.signum() < 0) { status.setSuccessful(false); throw new ApiProxy.ApplicationException( MemcacheServiceError.ErrorCode.INVALID_VALUE_VALUE, "Value to be incremented must be in the range of an unsigned 64-bit number"); } value = value.add(BigInteger.valueOf(delta)); if (value.signum() < 0) { value = UINT64_MIN_VALUE; } else if (value.compareTo(UINT64_MAX_VALUE) > 0) { value = value.and(UINT64_MAX_VALUE); } stats.recordDelete(ce); try { ce.value = value.toString().getBytes(UTF8); } catch (UnsupportedEncodingException e) { throw new ApiProxy.UnknownException(UTF8 + " encoding was not found."); } // don't change the flags; it keeps its original size/type ce.bytes = key.getBytes().length + ce.value.length; Map namespaceMap = getOrMakeSubMap(mockCache, namespace); namespaceMap.remove(key); namespaceMap.put(key, ce); stats.recordAdd(ce); result.setNewValue(value.longValue()); } status.setSuccessful(true); return result.build(); } public MemcacheBatchIncrementResponse batchIncrement( Status status, MemcacheBatchIncrementRequest batchReq) { MemcacheBatchIncrementResponse.Builder result = MemcacheBatchIncrementResponse.newBuilder(); String namespace = batchReq.getNameSpace(); synchronized (mockCache) { // only increment offers atomicity for (MemcacheIncrementRequest req : batchReq.getItemList()) { MemcacheIncrementResponse.Builder resp = MemcacheIncrementResponse.newBuilder(); Key key = new Key(req.getKey().toByteArray()); long delta = req.getDelta(); if (req.getDirection() == Direction.DECREMENT) { delta = -delta; } CacheEntry ce = getWithExpiration(namespace, key); long newvalue; if (ce == null) { if (req.hasInitialValue()) { MemcacheSerialization.ValueAndFlags value; try { value = MemcacheSerialization.serialize(req.getInitialValue()); } catch (IOException e) { throw new ApiProxy.UnknownException("Serialzation error: " + e); } ce = new CacheEntry(namespace, key, value.value, value.flags.ordinal(), 0); } else { stats.recordMiss(); resp.setIncrementStatus(IncrementStatusCode.NOT_CHANGED); result.addItem(resp); continue; } } stats.recordHit(ce); Long longval; try { longval = Long.parseLong(new String(ce.value, UTF8)); } catch (NumberFormatException e) { resp.setIncrementStatus(IncrementStatusCode.NOT_CHANGED); result.addItem(resp); continue; } catch (UnsupportedEncodingException e) { resp.setIncrementStatus(IncrementStatusCode.NOT_CHANGED); result.addItem(resp); continue; } if (longval < 0) { resp.setIncrementStatus(IncrementStatusCode.NOT_CHANGED); result.addItem(resp); continue; } newvalue = longval; newvalue += delta; if (delta < 0 && newvalue < 0) { newvalue = 0; } stats.recordDelete(ce); try { ce.value = Long.toString(newvalue).getBytes(UTF8); } catch (UnsupportedEncodingException e) { // Shouldn't happen. throw new ApiProxy.UnknownException(UTF8 + " encoding was not found."); } // don't change the flags; it keeps its original size/type ce.bytes = key.getBytes().length + ce.value.length; Map namespaceMap = getOrMakeSubMap(mockCache, namespace); namespaceMap.remove(key); namespaceMap.put(key, ce); stats.recordAdd(ce); resp.setIncrementStatus(IncrementStatusCode.OK); resp.setNewValue(newvalue); result.addItem(resp); } } status.setSuccessful(true); return result.build(); } public MemcacheFlushResponse flushAll(Status status, MemcacheFlushRequest req) { MemcacheFlushResponse.Builder result = MemcacheFlushResponse.newBuilder(); synchronized (mockCache) { mockCache.clear(); deleteHold.clear(); lru.clear(); stats = new LocalStats(0, 0, 0, 0, 0); } status.setSuccessful(true); return result.build(); } public MemcacheStatsResponse stats(Status status, MemcacheStatsRequest req) { MemcacheStatsResponse result = MemcacheStatsResponse.newBuilder().setStats(stats.getAsMergedNamespaceStats()).build(); status.setSuccessful(true); return result; } public long getMaxSizeInBytes() { return maxSize; } @Override public Integer getMaxApiRequestSize() { // Keep this in sync with MAX_REQUEST_SIZE in . return 32 << 20; // 32 MB } /* @VisibleForTesting */ LRU getLRU() { return lru; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy