Some time ago, NullPointerException was reported online, traced back to the code, and it was found that the service we relied on was suspended. We called the service in the Load method of GuavaCache, resulting in null cache after cache reloading, which would affect subsequent services. The problem was found. So what’s the solution? Let’s start with the code:

public LoadingCache<String, String> cache = CacheBuilder.newBuilder()
            .maximumSize(CACHE_MAXIMUN_SIZE)
            .refreshAfterWrite(REFRESH_DURATION, TimeUnit.MINUTES)
            .build(new CacheLoader<String, String>() {
                @Override
                public String load(String s) throws ExecutionException {
                    // This is the service we rely on to get the configuration from the platform
                    String config = configGateway.getConfig(s);
                    returnconfig; }});Copy the code

When the configGateway.getConfig() service is disabled, the obtained config is null, causing the original normal old values in the cache to be overwritten.

@Override
    public Result<List<String>> getConfig() {
        List<String> res = null;
        Map<String, Integer> map;
        try {
            String config = cache.get(KEY);
            map = GSON.fromJson(config, new TypeToken<Map<String, Integer>>() {
            }.getType());
            res = new ArrayList<>(map.keySet());
        } catch (ExecutionException e) {
            log.error("error:",e);
        }
        return Result.success(res);
    }
Copy the code

If config is null, map.keyset () will return null, and map.keyset () will return NPE. This does not solve the problem. The business requirement is to display the configuration properly. Displaying NULL is not sufficient. So how can we use the old value of cache in case of dependent service crash (preconditions: config update on platform is not frequent), at least make it normal display on app, so…

public LoadingCache<String, String> cache = CacheBuilder.newBuilder()
            .maximumSize(CACHE_MAXIMUN_SIZE)
            .refreshAfterWrite(REFRESH_DURATION, TimeUnit.MINUTES)
            .build(new CacheLoader<String, String>() {
                @Override
                public String load(String s) throws ExecutionException {
                    String config = configGateway.getConfig(s);
                    // When the dependent service crashes, we call return old value to override
                    if (Objects.isNull(config)) {
                        return cache.get(s);
                    }
                    returnconfig; }});Copy the code

When the dependent service crashes, we return the old value in the cache and let the old value overwrite the old value. When we get, we will get the normal data and avoid the NPE.

In extreme cases, when the service is just started, the dependent service is down, so the load needs to get, and the cache itself is null, so the get will have an infinite loop. Take a look at the source code:

public V get(K key) throws ExecutionException {
return this.localCache.getOrLoad(key);
}

V getOrLoad(K key) throws ExecutionException {
    return this.get(key, this.defaultLoader);
}

V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
    int hash = this.hash(Preconditions.checkNotNull(key));
    return this.segmentFor(hash).get(key, hash, loader);
}

V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
    Preconditions.checkNotNull(key);
    Preconditions.checkNotNull(loader);

    try {
        // For the first time, count is 0, skip if
        if (this.count ! =0) {
            LocalCache.ReferenceEntry<K, V> e = this.getEntry(key, hash);
            if(e ! =null) {
                long now = this.map.ticker.read();
                V value = this.getLiveValue(e, now);
                if(value ! =null) {
                    this.recordRead(e, now);
                    this.statsCounter.recordHits(1);
                    Object var17 = this.scheduleRefresh(e, key, hash, value, now, loader);
                    return var17;
                }

                LocalCache.ValueReference<K, V> valueReference = e.getValueReference();
                if (valueReference.isLoading()) {
                    Object var9 = this.waitForLoadingValue(e, key, valueReference);
                    returnvar9; }}}// The first call executes the lockedGetOrLoad function
        Object var15 = this.lockedGetOrLoad(key, hash, loader);
        return var15;
    } catch (ExecutionException var13) {
        Throwable cause = var13.getCause();
        if (cause instanceof Error) {
            throw new ExecutionError((Error)cause);
        } else if (cause instanceof RuntimeException) {
            throw new UncheckedExecutionException(cause);
        } else {
            throwvar13; }}finally {
        this.postReadCleanup(); }}V lockedGetOrLoad(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
    LocalCache.ValueReference<K, V> valueReference = null;
    LocalCache.LoadingValueReference<K, V> loadingValueReference = null;
    boolean createNewEntry = true;
    this.lock();

    LocalCache.ReferenceEntry e;
    try {
        long now = this.map.ticker.read();
        this.preWriteCleanup(now);
        int newCount = this.count - 1;
        AtomicReferenceArray<LocalCache.ReferenceEntry<K, V>> table = this.table;
        int index = hash & table.length() - 1;
        LocalCache.ReferenceEntry<K, V> first = (LocalCache.ReferenceEntry)table.get(index);
        // if e is null, skip the for loop
        for(e = first; e ! =null; e = e.getNext()) {
            K entryKey = e.getKey();
            if(e.getHash() == hash && entryKey ! =null && this.map.keyEquivalence.equivalent(key, entryKey)) {
                valueReference = e.getValueReference();
                if (valueReference.isLoading()) {
                    createNewEntry = false;
                } else {
                    V value = valueReference.get();
                    if (value == null) {
                        this.enqueueNotification(entryKey, hash, value, valueReference.getWeight(), RemovalCause.COLLECTED);
                    } else {
                        if (!this.map.isExpired(e, now)) {
                            this.recordLockedRead(e, now);
                            this.statsCounter.recordHits(1);
                            Object var16 = value;
                            return var16;
                        }

                        this.enqueueNotification(entryKey, hash, value, valueReference.getWeight(), RemovalCause.EXPIRED);
                    }

                    this.writeQueue.remove(e);
                    this.accessQueue.remove(e);
                    this.count = newCount;
                }
                break; }}if (createNewEntry) {
            loadingValueReference = new LocalCache.LoadingValueReference();
            if (e == null) {
                // Create a new entry
                e = this.newEntry(key, hash, first);
                e.setValueReference(loadingValueReference);
                table.set(index, e);
            } else{ e.setValueReference(loadingValueReference); }}}finally {
        this.unlock();
        this.postWriteCleanup();
    }

    if (createNewEntry) {
        Object var9;
        try {
            // Call loadSync to load data
            synchronized(e) {
                var9 = this.loadSync(key, hash, loadingValueReference, loader); }}finally {
            this.statsCounter.recordMisses(1);
        }

        return var9;
    } else {
        return this.waitForLoadingValue(e, key, valueReference); }}V getAndRecordStats(K key, int hash, LocalCache.LoadingValueReference<K, V> loadingValueReference, ListenableFuture<V> newValue) throws ExecutionException {
    Object value = null;

    Object var6;
    try {
        value = Uninterruptibles.getUninterruptibly(newValue);
        if (value == null) {
            throw new InvalidCacheLoadException("CacheLoader returned null for key " + key + ".");
        }

        this.statsCounter.recordLoadSuccess(loadingValueReference.elapsedNanos());
        this.storeLoadedValue(key, hash, loadingValueReference, value);
        var6 = value;
    } finally {
        if (value == null) {
            this.statsCounter.recordLoadException(loadingValueReference.elapsedNanos());
            / / cache behind Java. Lang. An IllegalStateException will to remove from the cache entry after operation
            this.removeLoadingValue(key, hash, loadingValueReference); }}return var6;
}
Copy the code

The LocalLoadingCache will discard the cache and execute removeLoadingValue to remove the key from the LocalLoadingCache.

Of course, this is not an infinite loop, but it is still null, but the probability of this happening is very low, and even if it does happen, it is not expected.