CacheController.java

/*
 * Copyright 2022 Global Crop Diversity Trust
 *
 * 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.gringlobal.api.admin.v1;

import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import javax.cache.management.CacheStatisticsMXBean;
import javax.management.MBeanServer;
import javax.management.MBeanServerInvocationHandler;
import javax.management.ObjectInstance;
import javax.management.ObjectName;

import lombok.extern.slf4j.Slf4j;
import org.gringlobal.api.v1.ApiBaseController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.jcache.JCacheCacheManager;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.hazelcast.core.DistributedObject;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.IMap;
import com.hazelcast.map.LocalMapStats;
import com.hazelcast.spring.cache.HazelcastCacheManager;

import io.swagger.annotations.Api;

@RestController("cacheApi1")
@PreAuthorize("hasAuthority('GROUP_ADMINS')")
@RequestMapping(CacheController.API_URL)
@Api(tags = { "cachev1" })
@Slf4j
public class CacheController {

	/** The Constant API_URL. */
	public static final String API_URL = ApiBaseController.APIv1_BASE + "/admin/cache";

	@Autowired
	private CacheManager cacheManager;

	@PostMapping(value = "/clearCache/clearAll")
	public void clearCacheAll() {
		for (String cacheName : cacheManager.getCacheNames()) {
			clearCache(cacheName);
		}
	}

	@PostMapping(value = "/clearCaches")
	public void clearCaches(@RequestBody final List<String> cacheNames) {
		for (String cacheName: cacheNames) {
			clearCache(cacheName);
		}
	}

	@PostMapping(value = "/clearCache/{name}")
	public void clearCache(@PathVariable("name") String cacheName) {
		final Cache cache = cacheManager.getCache(cacheName);
		if (cache != null) {
			log.info("Clearing cache {}", cacheName);
			cache.clear();
		} else {
			log.info("No such cache: {}", cacheName);
		}
	}

	@GetMapping(value = "", produces = { MediaType.APPLICATION_JSON_VALUE })
	public CacheStatsResponse cacheStats() {
		List<CacheStats> cacheMaps = new ArrayList<>();
		List<Object> cacheOther = new ArrayList<>();

		if (cacheManager instanceof HazelcastCacheManager) {
			cacheStatsForHazelcast(cacheMaps, cacheOther);
		} else if (cacheManager instanceof JCacheCacheManager) {
			cacheStatsForEhCache(cacheMaps, cacheOther);
		}

		return new CacheStatsResponse(cacheMaps, cacheOther);
	}

	private void cacheStatsForEhCache(List<CacheStats> cacheStatsList, List<Object> cacheOther) {
		try {
			final MBeanServer beanServer = ManagementFactory.getPlatformMBeanServer();
			final Set<ObjectInstance> cacheBeans = beanServer.queryMBeans(ObjectName.getInstance("javax.cache:type=CacheStatistics,CacheManager=*,Cache=*"), null);

			for (ObjectInstance cacheBean : cacheBeans) {
				final CacheStatisticsMXBean statistics = MBeanServerInvocationHandler
					.newProxyInstance(beanServer, cacheBean.getObjectName(), CacheStatisticsMXBean.class, false);

				var cacheStats = new CacheStats();
				cacheStats.serviceName = "ehcache";
				cacheStats.name = cacheBean.getObjectName().getKeyProperty("Cache");

				cacheStats.gets = statistics.getCacheGets();
				cacheStats.puts = statistics.getCachePuts();
				cacheStats.misses = statistics.getCacheMisses();
				cacheStats.hits = statistics.getCacheHits();
				cacheStats.hitPercentage = statistics.getCacheHitPercentage();
				cacheStats.removals = statistics.getCacheRemovals();
				cacheStats.evictions = statistics.getCacheEvictions();

				cacheStatsList.add(cacheStats);
			}
		} catch(Exception e){
			log.warn("Exception in cache statistic");
		}
	}

	private void cacheStatsForHazelcast(List<CacheStats> cacheMaps, List<Object> cacheOther) {
		Set<HazelcastInstance> instances = Hazelcast.getAllHazelcastInstances();
		for (HazelcastInstance hz : instances) {
			if (log.isDebugEnabled())
				log.debug("\n\nCache stats Instance: {}", hz.getName());

			for (DistributedObject o : hz.getDistributedObjects()) {
				if (o instanceof IMap) {
					IMap<?, ?> imap = (IMap<?, ?>) o;
					cacheMaps.add(new CacheStats(imap));

					if (log.isDebugEnabled()) {
						log.debug("{}: {} {}", imap.getServiceName(), imap.getName(), imap.getPartitionKey());
						LocalMapStats localMapStats = imap.getLocalMapStats();
						log.debug("created: {}", localMapStats.getCreationTime());
						log.debug("owned entries: {}", localMapStats.getOwnedEntryCount());
						log.debug("backup entries: {}", localMapStats.getBackupEntryCount());
						log.debug("locked entries: {}", localMapStats.getLockedEntryCount());
						log.debug("dirty entries: {}", localMapStats.getDirtyEntryCount());
						log.debug("hits: {}", localMapStats.getHits());
						log.debug("puts: {}", localMapStats.getPutOperationCount());
						log.debug("last update: {}", localMapStats.getLastUpdateTime());
						log.debug("last access: {}", localMapStats.getLastAccessTime());
					}
				} else {
					if (log.isDebugEnabled())
						log.debug("{} {}", o.getClass(), o);
					cacheOther.add(o);
				}
			}
		}
	}
	
	public static class CacheStatsResponse {
		public List<CacheStats> cacheMaps;
		public List<String> cacheOther;

		public CacheStatsResponse(List<CacheStats> cacheMaps, List<Object> cacheOther) {
			this.cacheMaps = cacheMaps;
			this.cacheOther = cacheOther.stream().map(Object::toString).collect(Collectors.toList());
		}
	}

	public static final class CacheStats {

		public String serviceName;
		public String name;

		/** @deprecated */
		@Deprecated
		public Long ownedEntryCount; // HZ specific
		/** @deprecated */
		@Deprecated
		public Long lockedEntryCount; // HZ specific

		public Long gets;
		public Long puts;

		public Long hits;
		public Float hitPercentage;
		public Long misses;
		public Long removals;
		public Long evictions;

		public CacheStats() {
		}

		public CacheStats(IMap<?, ?> imap) {
			this.serviceName = imap.getServiceName();
			this.name = imap.getName();

			this.ownedEntryCount = imap.getLocalMapStats().getOwnedEntryCount(); // HZ specific
			this.lockedEntryCount = imap.getLocalMapStats().getLockedEntryCount(); // HZ specific

			this.gets = imap.getLocalMapStats().getGetOperationCount();
			this.puts = imap.getLocalMapStats().getPutOperationCount();
			this.removals = imap.getLocalMapStats().getRemoveOperationCount();
			
			var nearCacheStats = imap.getLocalMapStats().getNearCacheStats();
			if (nearCacheStats != null) {
				this.evictions = nearCacheStats.getEvictions();
			}

			this.hits = imap.getLocalMapStats().getHits();
			this.misses = this.gets - this.hits;
			this.hitPercentage = this.gets != 0 ? 100f / this.gets * this.hits: 0;
		}

		public String getServiceName() {
			return serviceName;
		}

		public String getName() {
			return name;
		}

		/** @deprecated */
		@Deprecated
		public Long getOwnedEntryCount() {
			return ownedEntryCount;
		}

		/** @deprecated */
		@Deprecated
		public Long getLockedEntryCount() {
			return lockedEntryCount;
		}

		/** @deprecated */
		@Deprecated
		public Long getPutOperationCount() {
			return puts;
		}

		public Long getGets() {
			return gets;
		}

		public Long getPuts() {
			return puts;
		}

		public Long getMisses() {
			return misses;
		}

		public Long getHits() {
			return hits;
		}

		public Float getHitPercentage() {
			return hitPercentage;
		}

		public Long getRemovals() {
			return removals;
		}

		public Long getEvictions() {
			return evictions;
		}
	}
}