CacheController.java

/**
 * Copyright 2015 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.mvc.admin;

import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.StreamSupport;

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.admin.v1.CacheController.CacheStats;
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.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

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;

/**
 * Manage caches
 * 
 * @author mobreza
 */
@Controller
@RequestMapping("/admin/cache")
@PreAuthorize("hasAuthority('GROUP_ADMINS')")
@Slf4j
public class CacheController {

	@Autowired
	private CacheManager cacheManager;

	@RequestMapping(method = RequestMethod.POST, value = "/clearCache", params = { "clearAll" })
	public String clearCacheAll() {
		return clearCaches();
	}

	@RequestMapping(method = RequestMethod.POST, value = "/clearCaches")
	public String clearCaches() {
		for (String cacheName : cacheManager.getCacheNames()) {
			clearCache(cacheName);
		}
		return "redirect:/admin/cache/";
	}

	@RequestMapping(method = RequestMethod.POST, value = "/clearCache")
	public String clearCache(@RequestParam(required = true, value = "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);
		}
		return "redirect:/admin/cache/";
	}

	@RequestMapping(method = RequestMethod.POST, value = "/dump")
	public String dumpCache(@RequestParam(required = true, value = "name") String cacheName) {
		final Cache cache = cacheManager.getCache(cacheName);
		if (cache != null) {
			log.warn("Inspecting cache {} of type {}", cacheName, cache.getNativeCache().getClass());
			var nativeCache =  cache.getNativeCache();

			if (nativeCache instanceof Map<?,?>) {
				((Map<?, ?>) nativeCache).forEach((key, value) -> log.warn("Key={} [{}]: [{}]\n\t={}", Objects.toString(key), key.getClass(), value.getClass(), value));

			} else if (nativeCache instanceof javax.cache.Cache){
				StreamSupport.stream(((javax.cache.Cache<?, ?>) nativeCache).spliterator(), false)
					.filter(Objects::nonNull)
					.forEach(entry -> {
						var key = Objects.toString(entry.getKey());
						var value = entry.getValue();
						log.warn("Key={} [{}]: [{}]\n\t={}", key, key.getClass(), value.getClass(), value);
					});

			} else {
				log.warn("Can't dump cache of type: {}", nativeCache.getClass());
			}
		} else {
			log.info("No such cache: {}", cacheName);
		}
		return "redirect:/admin/cache/";
	}

	@GetMapping("/")
	public String cacheStats(Model model) {
		List<CacheStats> cacheMaps = new ArrayList<CacheStats>();
		List<Object> cacheOther = new ArrayList<Object>();

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

		cacheMaps.sort((a, b) -> a.getName().compareToIgnoreCase(b.getName()));
		model.addAttribute("cacheMaps", cacheMaps);
		model.addAttribute("cacheOther", cacheOther);

		return "/admin/cache";
	}

	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);
				}
			}
		}
	}

}