package com.coolretailer.ux.logic; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Set; import javax.annotation.PostConstruct; import com.coolretailer.ux.grpc.system.DataServiceGrpc; import com.coolretailer.ux.grpc.system.Query; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.cloud.sleuth.annotation.NewSpan; import org.springframework.data.redis.connection.RedisZSetCommands.Limit; import org.springframework.data.redis.connection.RedisZSetCommands.Range; import org.springframework.data.redis.core.BoundZSetOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import io.grpc.ClientInterceptor; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; @Component public class CacheProcessor implements ApplicationRunner { @Value("${grpc.server.name}") private String gRpcServerName; @Value("${grpc.server.port}") private String gRpcServerPort; private DataServiceGrpc.DataServiceBlockingStub dataserviceBlockingStub; private final List<ClientInterceptor> clientInterceptorList; public CacheProcessor(List<ClientInterceptor> clientInterceptorList) { this.clientInterceptorList = clientInterceptorList; } @Autowired private RedisTemplate<String, String> template; private BoundZSetOperations<String, String> getZSetOps() { return template.boundZSetOps("autocomplete"); } private BoundZSetOperations<String, String> getZSetOpsForMissingProducts() { return template.boundZSetOps("missing"); } private static final Logger LOGGER = LoggerFactory.getLogger(CacheProcessor.class); @PostConstruct private void init() { ManagedChannel managedChannel = ManagedChannelBuilder .forAddress(gRpcServerName, Integer.parseInt(gRpcServerPort)).usePlaintext() .intercept(clientInterceptorList).build(); dataserviceBlockingStub = DataServiceGrpc.newBlockingStub(managedChannel); LOGGER.info("#records: " + getZSetOps().zCard()); } @NewSpan public void run(ApplicationArguments args) throws Exception { if (args.containsOption("clear-cache")) { clearCache(); } if (args.containsOption("process-cache")) { List<String> cacheOptions = args.getOptionValues("process-cache"); if (cacheOptions.size() == 1) { processCache(cacheOptions.get(0)); } else { processCache(null); } if (args.containsOption("exit")) { Runtime.getRuntime().halt(0); } } } @NewSpan public List<String> getSuggestions(String searchPrefix) throws Exception { // Check in missing list first if (getZSetOpsForMissingProducts().rank(searchPrefix) != null) { return null; } Set<String> suggestions = getZSetOps().rangeByLex(Range.range().gte(searchPrefix).lte(searchPrefix + "xff"), Limit.limit().offset(0).count(10)); // not found in cache if (suggestions.size() == 0) { String queryString = "SELECT name FROM coolretailer.products where LOWER(name) like '" + searchPrefix.toLowerCase() + "%' LIMIT 10"; LOGGER.info("Cache miss :-( hitting BQ.."); List<String> results = processQuery(queryString, false); // not found in BQ if (CollectionUtils.isEmpty(results)) { LOGGER.info("No results from BQ. Adding to cache as missing product.."); getZSetOpsForMissingProducts().add(searchPrefix.toLowerCase(), 0); } return results; } List<String> results = new ArrayList<>(); Iterator<String> iterator = suggestions.iterator(); while (iterator.hasNext()) { String suggestion = iterator.next(); // product name is after ":" suggestion = suggestion.split(":")[1]; results.add(suggestion); } return results; } @NewSpan public void processCache(String limit) throws Exception { LOGGER.info("Calling BigQuery..."); LOGGER.info("Updating cache..."); StringBuffer qb = new StringBuffer("SELECT name FROM coolretailer.products"); if (limit != null) { qb.append(" LIMIT " + limit); } processQuery(qb.toString(), true); } @NewSpan public void clearCache() { template.delete("autocomplete"); LOGGER.info("Cleared cache."); } @NewSpan public List<String> processQuery(String queryString, boolean updateCache) throws Exception { Query query = Query.newBuilder().setQueryString(queryString).build(); List<String> names = new ArrayList<String>(); if (updateCache) { dataserviceBlockingStub.updateCache(query); LOGGER.info("Cache updated!"); } else { names = dataserviceBlockingStub.runQuery(query).getNameList(); } return names; } }