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

com.netflix.discovery.util.DeserializerStringCache Maven / Gradle / Ivy

There is a newer version: 0.40.13
Show newest version
package com.netflix.discovery.util;

import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.PrimitiveIterator.OfInt;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectReader;

/**
 * A non-locking alternative to {@link String#intern()} and {@link StringCache}
 * that works with Jackson's DeserializationContext. Definitely NOT thread-safe,
 * intended to avoid the costs associated with thread synchronization and
 * short-lived heap allocations (e.g., Strings)
 *
 */
public class DeserializerStringCache implements Function {

    public enum CacheScope {
        // Strings in this scope are freed on deserialization of each
        // Application element
        APPLICATION_SCOPE,
        // Strings in this scope are freed when overall deserialization is
        // completed
        GLOBAL_SCOPE
    }

    private static final Logger logger = LoggerFactory.getLogger(DeserializerStringCache.class);
    private static final boolean debugLogEnabled = logger.isDebugEnabled();
    private static final String ATTR_STRING_CACHE = "deserInternCache";
    private static final int LENGTH_LIMIT = 256;
    private static final int LRU_LIMIT = 1024 * 40;

    private final Map globalCache;
    private final Map applicationCache;
    private final int lengthLimit = LENGTH_LIMIT;

    /**
     * adds a new DeserializerStringCache to the passed-in ObjectReader
     * 
     * @param reader
     * @return a wrapped ObjectReader with the string cache attribute
     */
    public static ObjectReader init(ObjectReader reader) {
        return reader.withAttribute(ATTR_STRING_CACHE, new DeserializerStringCache(
                new HashMap(2048), new LinkedHashMap(4096, 0.75f, true) {
                    @Override
                    protected boolean removeEldestEntry(Entry eldest) {
                        return size() > LRU_LIMIT;
                    }

                }));
    }

    /**
     * adds an existing DeserializerStringCache from the DeserializationContext
     * to an ObjectReader
     * 
     * @param reader
     *            a new ObjectReader
     * @param context
     *            an existing DeserializationContext containing a
     *            DeserializerStringCache
     * @return a wrapped ObjectReader with the string cache attribute
     */
    public static ObjectReader init(ObjectReader reader, DeserializationContext context) {
        return withCache(context, cache -> {
            if (cache == null)
                throw new IllegalStateException();
            return reader.withAttribute(ATTR_STRING_CACHE, cache);
        });
    }

    /**
     * extracts a DeserializerStringCache from the DeserializationContext
     * 
     * @param context
     *            an existing DeserializationContext containing a
     *            DeserializerStringCache
     * @return a wrapped ObjectReader with the string cache attribute
     */
    public static DeserializerStringCache from(DeserializationContext context) {
        return withCache(context, cache -> {
            if (cache == null) {
                cache = new DeserializerStringCache(new HashMap(),
                        new HashMap());
            }
            return cache;
        });
    }

    /**
     * clears app-scoped cache entries from the specified ObjectReader
     * 
     * @param reader
     */
    public static void clear(ObjectReader reader) {
        clear(reader, CacheScope.APPLICATION_SCOPE);
    }

    /**
     * clears cache entries in the given scope from the specified ObjectReader.
     * Always clears app-scoped entries.
     * 
     * @param reader
     * @param scope
     */
    public static void clear(ObjectReader reader, final CacheScope scope) {
        withCache(reader, cache -> {
            if (scope == CacheScope.GLOBAL_SCOPE) {
                if (debugLogEnabled)
                    logger.debug("clearing global-level cache with size {}", cache.globalCache.size());
                cache.globalCache.clear();
            }
            if (debugLogEnabled)
                logger.debug("clearing app-level serialization cache with size {}", cache.applicationCache.size());
            cache.applicationCache.clear();
            return null;
        });
    }

    /**
     * clears app-scoped cache entries from the specified DeserializationContext
     * 
     * @param context
     */
    public static void clear(DeserializationContext context) {
        clear(context, CacheScope.APPLICATION_SCOPE);
    }

    /**
     * clears cache entries in the given scope from the specified
     * DeserializationContext. Always clears app-scoped entries.
     * 
     * @param context
     * @param scope
     */
    public static void clear(DeserializationContext context, CacheScope scope) {
        withCache(context, cache -> {
            if (scope == CacheScope.GLOBAL_SCOPE) {
                if (debugLogEnabled)
                    logger.debug("clearing global-level serialization cache with size {}", cache.globalCache.size());
                cache.globalCache.clear();
            }
            if (debugLogEnabled)
                logger.debug("clearing app-level serialization cache with size {}", cache.applicationCache.size());
            cache.applicationCache.clear();
            return null;
        });
    }

    private static  T withCache(DeserializationContext context, Function consumer) {
        DeserializerStringCache cache = (DeserializerStringCache) context.getAttribute(ATTR_STRING_CACHE);
        return consumer.apply(cache);
    }

    private static  T withCache(ObjectReader reader, Function consumer) {
        DeserializerStringCache cache = (DeserializerStringCache) reader.getAttributes()
                .getAttribute(ATTR_STRING_CACHE);
        return consumer.apply(cache);
    }

    private DeserializerStringCache(Map globalCache, Map applicationCache) {
        this.globalCache = globalCache;
        this.applicationCache = applicationCache;
    }

    public ObjectReader initReader(ObjectReader reader) {
        return reader.withAttribute(ATTR_STRING_CACHE, this);
    }

    /**
     * returns a String read from the JsonParser argument's current position.
     * The returned value may be interned at the app scope to reduce heap
     * consumption
     * 
     * @param jp
     * @return a possibly interned String
     * @throws IOException
     */
    public String apply(final JsonParser jp) throws IOException {
        return apply(jp, CacheScope.APPLICATION_SCOPE, null);
    }

    public String apply(final JsonParser jp, CacheScope cacheScope) throws IOException {
        return apply(jp, cacheScope, null);
    }

    /**
     * returns a String read from the JsonParser argument's current position.
     * The returned value may be interned at the given cacheScope to reduce heap
     * consumption
     * 
     * @param jp
     * @param cacheScope
     * @return a possibly interned String
     * @throws IOException
     */
    public String apply(final JsonParser jp, CacheScope cacheScope, Supplier source) throws IOException {
        return apply(CharBuffer.wrap(jp, source), cacheScope);
    }

    /**
     * returns a String that may be interned at app-scope to reduce heap
     * consumption
     * 
     * @param charValue
     * @return a possibly interned String
     */
    public String apply(final CharBuffer charValue) {
        return apply(charValue, CacheScope.APPLICATION_SCOPE);
    }

    /**
     * returns a object of type T that may be interned at the specified scope to
     * reduce heap consumption
     * 
     * @param charValue
     * @param cacheScope
     * @param trabsform
     * @return a possibly interned instance of T
     */
    public String apply(CharBuffer charValue, CacheScope cacheScope) {
        int keyLength = charValue.length();
        if ((lengthLimit < 0 || keyLength <= lengthLimit)) {
            Map cache = (cacheScope == CacheScope.GLOBAL_SCOPE) ? globalCache : applicationCache;
            String value = cache.get(charValue);
            if (value == null) {
                value = charValue.consume((k, v) -> {
                    cache.put(k, v);
                });
            } else {
                // System.out.println("cache hit");
            }
            return value;
        }
        return charValue.toString();
    }

    /**
     * returns a String that may be interned at the app-scope to reduce heap
     * consumption
     * 
     * @param stringValue
     * @return a possibly interned String
     */
    @Override
    public String apply(final String stringValue) {
        return apply(stringValue, CacheScope.APPLICATION_SCOPE);
    }

    /**
     * returns a String that may be interned at the given scope to reduce heap
     * consumption
     * 
     * @param stringValue
     * @param cacheScope
     * @return a possibly interned String
     */
    public String apply(final String stringValue, CacheScope cacheScope) {
        if (stringValue != null && (lengthLimit < 0 || stringValue.length() <= lengthLimit)) {
            return (String) (cacheScope == CacheScope.GLOBAL_SCOPE ? globalCache : applicationCache)
                    .computeIfAbsent(CharBuffer.wrap(stringValue), s -> {
                        logger.trace(" (string) writing new interned value {} into {} cache scope", stringValue, cacheScope);
                        return stringValue;
                    });
        }
        return stringValue;
    }

    public int size() {
        return globalCache.size() + applicationCache.size();
    }

    private interface CharBuffer {

        public static CharBuffer wrap(JsonParser source, Supplier stringSource) throws IOException {
            return new ArrayCharBuffer(source, stringSource);
        }

        public static CharBuffer wrap(JsonParser source) throws IOException {
            return new ArrayCharBuffer(source);
        }

        public static CharBuffer wrap(String source) {
            return new StringCharBuffer(source);
        }

        String consume(BiConsumer valueConsumer);

        int length();

        OfInt chars();

        static class ArrayCharBuffer implements CharBuffer {
            private char[] source;
            private int offset;
            private final int length;
            private int hash;
            private Supplier valueTransform;

            ArrayCharBuffer(JsonParser source) throws IOException {
                this.source = source.getTextCharacters();
                this.offset = source.getTextOffset();
                this.length = source.getTextLength();
            }

            ArrayCharBuffer(JsonParser source, Supplier valueTransform) throws IOException {
                this(source);
                this.valueTransform = valueTransform;
            }

            @Override
            public int length() {
                return length;
            }

            @Override
            public int hashCode() {
                if (hash == 0 && length != 0) {
                    hash = arrayHash(source, offset, length);
                }
                return hash;
            }

            @Override
            public boolean equals(Object other) {
                if (other instanceof CharBuffer) {
                    CharBuffer otherBuffer = (CharBuffer) other;
                    if (otherBuffer.length() == length) {
                        OfInt otherText = otherBuffer.chars();
                        for (int i = offset; i < length; i++) {
                            if (source[i] != otherText.nextInt()) {
                                return false;
                            }
                        }
                        return true;
                    }
                }
                return false;
            }

            @Override
            public OfInt chars() {
                return new OfInt() {
                    int index = offset;
                    int limit = index + length;

                    @Override
                    public boolean hasNext() {
                        return index < limit;
                    }

                    @Override
                    public int nextInt() {
                        return source[index++];
                    }
                };
            }

            @Override
            public String toString() {
                return new String(this.source, offset, length);
            }

            @Override
            public String consume(BiConsumer valueConsumer) {
                String key = new String(this.source, offset, length);
                String value = valueTransform == null ? key : valueTransform.get();
                valueConsumer.accept(new StringCharBuffer(key), value);
                return value;
            }

            private static int arrayHash(char[] a, int offset, int length) {
                if (a == null)
                    return 0;
                int result = 0;
                int limit = offset + length;
                for (int i = offset; i < limit; i++) {
                    result = 31 * result + a[i];
                }
                return result;
            }
        }

        static class StringCharBuffer implements CharBuffer {
            private String source;

            StringCharBuffer(String source) {
                this.source = source;
            }

            @Override
            public int hashCode() {
                return source.hashCode();
            }

            @Override
            public boolean equals(Object other) {
                if (other instanceof CharBuffer) {
                    CharBuffer otherBuffer = (CharBuffer) other;
                    int length = source.length();
                    if (otherBuffer.length() == length) {
                        OfInt otherText = otherBuffer.chars();
                        for (int i = 0; i < length; i++) {
                            if (source.charAt(i) != otherText.nextInt()) {
                                return false;
                            }
                        }
                        return true;
                    }
                }
                return false;
            }

            @Override
            public int length() {
                return source.length();
            }

            @Override
            public String toString() {
                return source;
            }

            @Override
            public OfInt chars() {
                return new OfInt() {
                    int index;

                    @Override
                    public boolean hasNext() {
                        return index < source.length();
                    }

                    @Override
                    public int nextInt() {
                        return source.charAt(index++);
                    }
                };
            }

            @Override
            public String consume(BiConsumer valueConsumer) {
                valueConsumer.accept(this, source);
                return source;
            }
        }

    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy