Java tutorial
/* * Copyright 2014 Ran Meng * * 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 com.github.totyumengr.minicubes.cluster; import java.io.Serializable; import java.math.BigDecimal; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import javax.sql.DataSource; import md.math.DoubleDouble; import org.roaringbitmap.RoaringBitmap; 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.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.PreparedStatementCreator; import org.springframework.jdbc.core.RowCallbackHandler; import org.springframework.jdbc.core.SqlParameterValue; import org.springframework.jdbc.core.SqlTypeValue; import org.springframework.jdbc.core.StatementCreatorUtils; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import com.github.totyumengr.minicubes.core.FactTable.FactTableBuilder; import com.github.totyumengr.minicubes.core.MiniCube; import com.hazelcast.config.Config; import com.hazelcast.config.ExecutorConfig; import com.hazelcast.config.GroupConfig; import com.hazelcast.config.JoinConfig; import com.hazelcast.config.ListenerConfig; import com.hazelcast.config.ManagementCenterConfig; import com.hazelcast.config.MulticastConfig; import com.hazelcast.config.NetworkConfig; import com.hazelcast.config.TcpIpConfig; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.core.HazelcastInstanceAware; import com.hazelcast.core.IMap; import com.hazelcast.core.Member; import com.hazelcast.core.MemberAttributeEvent; import com.hazelcast.core.MembershipEvent; import com.hazelcast.core.MembershipListener; import com.hazelcast.core.MultiExecutionCallback; /** * Implementation beyond {@link Hazelcast}. * * <p>{@link #factSourceSql} is important, we use the {@link ResultSetMetaData#getColumnLabel(int)} as meta data. * Please make sure to put dimension in <b>front</b> of measure, and you should classify the split * point by {@link #splitIndex index from 0}. On the other let it equal <code>-1</code> then we try to split by meta data: * start with <b>dim_</b>, we use it as dimension. * * <p>{@link #factSourceSql} must have a <code>?</code> so we will specify this when {@link #reassignRole(String, String)} * @author mengran * */ @Service @Configuration public class TimeSeriesMiniCubeManagerHzImpl implements TimeSeriesMiniCubeManager { private static final Logger LOGGER = LoggerFactory.getLogger(TimeSeriesMiniCubeManagerHzImpl.class); private static final String DISTRIBUTED_EXECUTOR = "distributedExecutor"; private static final String MINICUBE_MANAGER = "minicubeManager"; private static final ThreadLocal<String[]> AGG_CONTEXT = new ThreadLocal<String[]>(); @Autowired private Environment env; @Autowired private HazelcastInstance hazelcastInstance; private String hzGroupName; @Value("${hazelcast.executor.timeout}") private int hzExecutorTimeout; @Autowired private DataSource dataSource; @Value("${minicube.builder.mergeFlagColumn}") private String mergeFlagColumn; @Value("${minicube.builder.sourceSql}") private String factSourceSql; @Value("${minicube.measure.fromIndex}") private int splitIndex = -1; /** * Manage target object. */ private transient MiniCube miniCube; private static final AtomicInteger MINICUBE_PK = new AtomicInteger(0); private ScheduledExecutorService handleNewMember = Executors.newSingleThreadScheduledExecutor(); @Bean public HazelcastInstance hazelcastServer() { Config hazelcastConfig = new Config("minicubes-cluster"); hazelcastConfig.setProperty("hazelcast.system.log.enabled", "false"); hazelcastConfig.setProperty("hazelcast.logging.type", "slf4j"); hazelcastConfig.setProperty("hazelcast.jmx", "true"); hazelcastConfig.setProperty("hazelcast.jmx.detailed", "true"); if (StringUtils.hasText(env.getRequiredProperty("hazelcast.operation.thread.count"))) { hazelcastConfig.setProperty("hazelcast.operation.thread.count", env.getRequiredProperty("hazelcast.operation.thread.count")); } hazelcastConfig .setGroupConfig(new GroupConfig(hzGroupName = env.getRequiredProperty("hazelcast.group.name"), env.getRequiredProperty("hazelcast.group.password"))); if (StringUtils.hasText(env.getRequiredProperty("hazelcast.mancenter.url"))) { hazelcastConfig.setManagementCenterConfig(new ManagementCenterConfig().setEnabled(true) .setUrl(env.getRequiredProperty("hazelcast.mancenter.url"))); } String hzMembers; JoinConfig joinConfig = new JoinConfig(); if (!StringUtils.hasText(hzMembers = env.getRequiredProperty("hazelcast.members"))) { // Means multiple cast joinConfig.setMulticastConfig(new MulticastConfig().setEnabled(true)); } else { joinConfig.setMulticastConfig(new MulticastConfig().setEnabled(false)) .setTcpIpConfig(new TcpIpConfig().setEnabled(true).addMember(hzMembers)); } hazelcastConfig.setNetworkConfig(new NetworkConfig().setJoin(joinConfig)); // New ExecutorService int hzExecutorSize = -1; if ((hzExecutorSize = env.getRequiredProperty("hazelcast.executor.poolsize", Integer.class)) < 0) { hzExecutorSize = Runtime.getRuntime().availableProcessors(); LOGGER.info("Use cpu core count {} setting into executor service", hzExecutorSize); } hazelcastConfig.addExecutorConfig(new ExecutorConfig(DISTRIBUTED_EXECUTOR, hzExecutorSize) .setQueueCapacity(env.getRequiredProperty("hazelcast.executor.queuecapacity", Integer.class))); // Add member event listener hazelcastConfig.addListenerConfig(new ListenerConfig().setImplementation(new MembershipListener() { @Override public void memberRemoved(MembershipEvent membershipEvent) { // Mean a member leave out of cluster. LOGGER.info("A member {} has removed from cluster {}", membershipEvent.getMember(), membershipEvent.getCluster().getClusterTime()); IMap<String, String> miniCubeManager = hazelcastInstance.getMap(MINICUBE_MANAGER); LOGGER.info("Minicube manager status {}", ObjectUtils.getDisplayString(miniCubeManager.entrySet())); // FIXME: Schedule to remove relationship after "long disconnect". } @Override public void memberAttributeChanged(MemberAttributeEvent memberAttributeEvent) { // Let it empty. } @Override public void memberAdded(MembershipEvent membershipEvent) { // Mean a member join into cluster. LOGGER.info("A member {} has joined into cluster {}, let it's self to claim a role.", membershipEvent.getMember(), membershipEvent.getCluster().getClusterTime()); } })); HazelcastInstance instance = Hazelcast.newHazelcastInstance(hazelcastConfig); // Put execute context instance.getUserContext().put("this", TimeSeriesMiniCubeManagerHzImpl.this); // Handle new member LOGGER.info("Handle new member {} came in after 1 minute.", instance.getCluster().getLocalMember()); // Set member's status to load-pending, this will effect #reassignRole instance.getCluster().getLocalMember().setBooleanAttribute("load-pending", true); handleNewMember.schedule(new Runnable() { @Override public void run() { handleNewMember(instance, instance.getCluster().getLocalMember()); } }, 60, TimeUnit.SECONDS); return instance; } private void handleNewMember(HazelcastInstance instance, Member member) { // Relationship between Member and MiniCube ID IMap<String, String> miniCubeManager = instance.getMap(MINICUBE_MANAGER); LOGGER.info("Minicube manager status {}", ObjectUtils.getDisplayString(miniCubeManager)); String key = member.getSocketAddress().toString(); // FIXME: load-pending status need refactor instance.getCluster().getLocalMember().setBooleanAttribute("load-pending", false); if (miniCubeManager.containsKey(key) && !miniCubeManager.get(key).startsWith("?")) { // Maybe node-restart LOGGER.info("A node{} restarted, so we need rebuild cube{}", key, miniCubeManager.get(key)); // Reassign task. reassignRole(miniCubeManager.get(key), miniCubeManager.get(key).split("::")[0]); } else { // First time join into cluster String id = "?" + "::" + hzGroupName + "@" + key; miniCubeManager.put(key, id); member.setStringAttribute("cubeId", id); LOGGER.info("Add {} into cluster {}", id, hzGroupName); } LOGGER.info("Set load-pending status to false, enable reassign feature on {}", member); } // ------------------------------ Implementation ------------------------------ @Override public <T> List<T> execute(Callable<T> task, Collection<String> cubeIds, int timeoutSeconds) { Set<Member> members = hazelcastInstance.getCluster().getMembers(); Set<Member> selected = new LinkedHashSet<Member>(); if (cubeIds != null && !cubeIds.isEmpty()) { List<String> cubeNodes = cubeIds.stream().map(e -> e.split("@")[1]).collect(Collectors.toList()); for (Member m : members) { if (cubeNodes.contains(m.getSocketAddress().toString())) { selected.add(m); } } } else { selected.addAll(members); LOGGER.warn("Select all members {} in cluster to execute on.", selected); } final int size = selected.size(); LOGGER.debug("Start to run task {} on {}", task, selected); // Call distributed execute service to run it. final List<T> result = new ArrayList<T>(selected.size()); final List<Exception> exceptionResult = new ArrayList<Exception>(); CountDownLatch cdl = new CountDownLatch(1); AtomicInteger completedCount = new AtomicInteger(0); hazelcastInstance.getExecutorService(DISTRIBUTED_EXECUTOR).submitToMembers(task, selected, new MultiExecutionCallback() { @SuppressWarnings("unchecked") @Override public void onResponse(Member member, Object value) { int i = completedCount.incrementAndGet(); LOGGER.debug("Completed of {}/{}, {} and {}", i, size, member, value); if (value instanceof Exception) { exceptionResult.add((Exception) value); } else { result.add((T) value); } } @Override public void onComplete(Map<Member, Object> values) { LOGGER.info("Successfully execute {} on cluster, collect {} result.", task, values.size()); cdl.countDown(); } }); if (completedCount.get() < size) { // FIXME: When some task do not executed. Maybe reject? error? } try { cdl.await(timeoutSeconds > 0 ? timeoutSeconds : Integer.MAX_VALUE, TimeUnit.SECONDS); } catch (InterruptedException e) { // Ignore } // Exception handled if (!exceptionResult.isEmpty()) { LOGGER.error("{} exceptions occurred when try to execute {} on {}", exceptionResult.size(), task, ObjectUtils.getDisplayString(selected)); for (int i = 0; i < exceptionResult.size(); i++) { LOGGER.error("#1 exception === ", exceptionResult.get(i)); } throw new RuntimeException("Exception occurred when try to execute, please see detail logs above."); } return result; } private static abstract class CubeBuilder implements Callable<String>, HazelcastInstanceAware, Serializable { /** * */ private static final long serialVersionUID = 1L; protected transient HazelcastInstance instance; protected transient TimeSeriesMiniCubeManagerHzImpl impl; protected String cubeId; protected String timeSeries; public CubeBuilder(String cubeId, String timeSeries) { super(); this.cubeId = cubeId; this.timeSeries = timeSeries; } @Override public void setHazelcastInstance(HazelcastInstance hazelcastInstance) { instance = hazelcastInstance; impl = (TimeSeriesMiniCubeManagerHzImpl) instance.getUserContext().get("this"); } protected String pre() { return null; } protected String post(MiniCube newMiniCube) { return null; } protected String sql() { return impl.factSourceSql; } @Override public String call() throws Exception { Member localMember = instance.getCluster().getLocalMember(); String member = cubeId.split("@")[1]; if (!member.equals(localMember.getSocketAddress().toString())) { throw new UnsupportedOperationException( "Assignment " + cubeId + " " + timeSeries + "only permitted to run in local member."); } String forReturn = pre(); if (StringUtils.hasLength(forReturn)) { LOGGER.info("Don't fetch data and directly return {} {}.", cubeId, timeSeries); return forReturn; } LOGGER.info("Start fetching data and building cube {}, {}", cubeId, timeSeries); JdbcTemplate template = new JdbcTemplate(impl.dataSource); FactTableBuilder builder = new FactTableBuilder(); boolean builded = false; AtomicInteger rowCount = new AtomicInteger(); try { builder.build(timeSeries); AtomicBoolean processMeta = new AtomicBoolean(true); AtomicInteger actualSplitIndex = new AtomicInteger(); List<SqlParameterValue> params = new ArrayList<SqlParameterValue>(); if (timeSeries.length() == 8 && timeSeries.toUpperCase().contains("X")) { // Means one XUN's data String m = timeSeries.toUpperCase().split("X")[0]; int x = Integer.parseInt(timeSeries.toUpperCase().split("X")[1]); Assert.isTrue(x > 0 && x < 4, "Only support pattern yyyymmX[1-3]. " + timeSeries); int s = (x - 1) * 10 + 1; SqlParameterValue start = new SqlParameterValue(SqlTypeValue.TYPE_UNKNOWN, m + (s > 9 ? s : ("0" + s))); SqlParameterValue end = null; switch (x) { case 1: end = new SqlParameterValue(SqlTypeValue.TYPE_UNKNOWN, m + "10"); break; case 2: end = new SqlParameterValue(SqlTypeValue.TYPE_UNKNOWN, m + "20"); break; case 3: end = new SqlParameterValue(SqlTypeValue.TYPE_UNKNOWN, m + "31"); break; default: break; } params.add(start); params.add(end); } else if (timeSeries.length() == 8) { // Means one day's data SqlParameterValue v = new SqlParameterValue(SqlTypeValue.TYPE_UNKNOWN, timeSeries); params.add(v); } else if (timeSeries.length() == 6 && !timeSeries.toUpperCase().contains("Q")) { // Means one month's data SqlParameterValue start = new SqlParameterValue(SqlTypeValue.TYPE_UNKNOWN, timeSeries + "01"); SqlParameterValue end = new SqlParameterValue(SqlTypeValue.TYPE_UNKNOWN, timeSeries + "31"); params.add(start); params.add(end); } else if (timeSeries.length() == 6 && timeSeries.toUpperCase().contains("Q")) { // Means one Q's data String y = timeSeries.toUpperCase().split("Q")[0]; int q = Integer.parseInt(timeSeries.toUpperCase().split("Q")[1]); Assert.isTrue(q > 0 && q < 5, "Only support pattern yyyyQ[1-4]. " + timeSeries); int m = ((q - 1) * 3 + 1); // Fix #3 SqlParameterValue start = new SqlParameterValue(SqlTypeValue.TYPE_UNKNOWN, y + (m > 9 ? m : ("0" + m)) + "01"); m = (q * 3); SqlParameterValue end = new SqlParameterValue(SqlTypeValue.TYPE_UNKNOWN, y + (m > 9 ? m : ("0" + m)) + "31"); params.add(start); params.add(end); } else { throw new IllegalArgumentException("Only supported day or month format." + timeSeries); } LOGGER.info("Start to fetch data {}", params.stream().map(new Function<SqlParameterValue, Object>() { @Override public Object apply(SqlParameterValue t) { return t.getValue(); } }).reduce("", (x, y) -> x + "~" + y)); template.query(new PreparedStatementCreator() { @Override public PreparedStatement createPreparedStatement(Connection con) throws SQLException { PreparedStatement stmt = con.prepareStatement(sql(), ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); // MySQL steaming result-set http://dev.mysql.com/doc/connector-j/en/connector-j-reference-implementation-notes.html // http://stackoverflow.com/questions/2095490/how-to-manage-a-large-dataset-using-spring-mysql-and-rowcallbackhandler try { stmt.setFetchSize(Integer.MIN_VALUE); LOGGER.info("Set stream feature of MySQL. {}", stmt.getFetchSize()); } catch (Exception e) { // Ignore maybe do not supported. } for (int i = 0; i < params.size(); i++) { StatementCreatorUtils.setParameterValue(stmt, i + 1, params.get(i), params.get(i).getValue()); } return stmt; } }, new RowCallbackHandler() { @Override public void processRow(ResultSet rs) throws SQLException { if (processMeta.get()) { ResultSetMetaData meta = rs.getMetaData(); int dimSize = 0; if (impl.splitIndex < 0) { LOGGER.debug("Not specify splitIndex so we guess by column labels"); for (int i = 1; i <= meta.getColumnCount(); i++) { if (meta.getColumnLabel(i).toLowerCase().startsWith("dim_")) { LOGGER.debug("Add dim column {}", meta.getColumnLabel(i)); builder.addDimColumns( Arrays.asList(new String[] { meta.getColumnLabel(i) })); dimSize++; } else { LOGGER.debug("Add measure column {}", meta.getColumnLabel(i)); builder.addIndColumns( Arrays.asList(new String[] { meta.getColumnLabel(i) })); } } } else { LOGGER.debug("Specify splitIndex {} means measure start."); for (int i = 1; i <= meta.getColumnCount(); i++) { if (i < impl.splitIndex) { LOGGER.debug("Add dim column {}", meta.getColumnLabel(i)); builder.addDimColumns( Arrays.asList(new String[] { meta.getColumnLabel(i) })); dimSize++; } else { LOGGER.debug("Add measure column {}", meta.getColumnLabel(i)); builder.addIndColumns( Arrays.asList(new String[] { meta.getColumnLabel(i) })); } } } actualSplitIndex.set(dimSize); // End meta setting processMeta.set(false); } // Add fact data List<Integer> dimDatas = new ArrayList<Integer>(actualSplitIndex.get()); List<DoubleDouble> indDatas = new ArrayList<DoubleDouble>( rs.getMetaData().getColumnCount() - dimDatas.size()); for (int i = 0; i < rs.getMetaData().getColumnCount(); i++) { if (i < actualSplitIndex.get()) { dimDatas.add(rs.getInt(i + 1)); } else { indDatas.add(DoubleDouble.valueOf(rs.getDouble(i + 1))); } } rowCount.incrementAndGet(); int pk = MINICUBE_PK.incrementAndGet(); builder.addDimDatas(pk, dimDatas); builder.addIndDatas(pk, indDatas); if (rowCount.get() % 1000000 == 0) { LOGGER.info("Loaded {} records into cube.", rowCount.get()); } } }); // Ending build operation MiniCube newMiniCube = new MiniCube(builder.done()); builded = true; String newCubeId = post(newMiniCube); return newCubeId; } finally { if (!builded) { builder.done(); } } } } private static class Assign extends CubeBuilder implements Callable<String>, HazelcastInstanceAware, Serializable { /** * */ private static final long serialVersionUID = 1L; public Assign(String cubeId, String timeSeries) { super(cubeId, timeSeries); } @Override protected String pre() { Member localMember = instance.getCluster().getLocalMember(); String member = cubeId.split("@")[1]; // Check load-pending status boolean loadPending = localMember.getBooleanAttribute("load-pending"); if (loadPending) { String newCubeId = timeSeries + "::" + impl.hzGroupName + "@" + member; IMap<String, String> miniCubeManager = instance.getMap(MINICUBE_MANAGER); miniCubeManager.put(member, newCubeId); LOGGER.warn("Only change relationship {} {} when load-pending status.", member, newCubeId); return newCubeId; } return null; } @Override protected String post(MiniCube newMiniCube) { Member localMember = instance.getCluster().getLocalMember(); String member = cubeId.split("@")[1]; // Ending build operation impl.miniCube = newMiniCube; String newCubeId = timeSeries + "::" + impl.hzGroupName + "@" + member; LOGGER.info("Success to build cube {} from {} and {}", newCubeId, cubeId, timeSeries); // Put relationship into member localMember.setStringAttribute("cubeId", newCubeId); IMap<String, String> miniCubeManager = instance.getMap(MINICUBE_MANAGER); miniCubeManager.put(member, newCubeId); return newCubeId; } } @Override public String reassignRole(String cubeId, String timeSeries) { LOGGER.info("Starting to assign {} to calculating {} calculation.", cubeId, timeSeries); // FIXME: Validation with Spring MVC, so we do not double-check it. Enhancement in future version. if (cubeId.startsWith("?")) { LOGGER.debug("First time assign {}", cubeId); } // Do it in self VM List<String> result = execute(new Assign(cubeId, timeSeries), Arrays.asList(new String[] { cubeId }), -1); Assert.hasText(result.get(0), "Fail to assign " + cubeId + " " + timeSeries); LOGGER.info("Successfully reassign role {}", result.get(0)); return result.get(0); } private static class Merge extends CubeBuilder implements Callable<String>, HazelcastInstanceAware, Serializable { /** * */ private static final long serialVersionUID = 1L; private int version; public Merge(String cubeId, String timeSeries, int version) { super(cubeId, timeSeries); this.version = version; } @Override protected String pre() { return null; } @Override protected String post(MiniCube newMiniCube) { // Ending build operation impl.miniCube.merge(newMiniCube); LOGGER.info("Success to merge cube {} into {} of ", newMiniCube, impl.miniCube, timeSeries); return cubeId; } @Override protected String sql() { String originalSql = super.sql(); // FIXME: Weak logic of SQL String sql = originalSql + " and " + impl.mergeFlagColumn + " = " + version; LOGGER.info("Merge data range {} of {}.", sql, timeSeries); return sql; } } @Override public int merge(String timeSeries, int version) { LOGGER.info("Starting to merge {}...", timeSeries); try { Collection<String> cubeIds = cubeIds(timeSeries); // FIXME: When support #3, this assert will be fail. Assert.isTrue(cubeIds.size() == 1, "Current version have not support multiple cubes of one time-series."); // Do execute List<String> results = execute(new Merge(cubeIds.iterator().next(), timeSeries, version), cubeIds, hzExecutorTimeout); LOGGER.info("Merge {} of {} sucessfully, result is {}.", timeSeries, version, results); } finally { AGG_CONTEXT.remove(); } // FIXME: Current version we don't return this value. return -1; } @Override public Collection<String> allCubeIds() { Set<Member> members = hazelcastInstance.getCluster().getMembers(); // Exact match return members.stream().filter(e -> e.getStringAttribute("cubeId") != null) .map(e -> e.getStringAttribute("cubeId")).collect(Collectors.toList()); } @Override public Collection<String> cubeIds(String cubeDate) { Set<Member> members = hazelcastInstance.getCluster().getMembers(); // Exact match return members.stream().filter(e -> e.getStringAttribute("cubeId").split("::")[0].startsWith(cubeDate)) .map(e -> e.getStringAttribute("cubeId")).collect(Collectors.toList()); } private static class Mode extends Executee implements Callable<Void> { /** * */ private static final long serialVersionUID = 1L; private boolean parallelMode; public Mode(boolean parallelMode) { super(); this.parallelMode = parallelMode; } @Override public Void call() throws Exception { LOGGER.info("Set model {} on {}", parallelMode, instance.getCluster().getLocalMember()); if (impl.miniCube != null) { impl.miniCube.setParallelMode(parallelMode); } return null; } } @Override public void setMode(boolean parallelModel) { try { Set<String> cubeIds = cubeIds(); // Do execute execute(new Mode(parallelModel), cubeIds, hzExecutorTimeout); LOGGER.info("Set stream's mode {} on {}.", parallelModel, cubeIds); } finally { AGG_CONTEXT.remove(); } } @Override public TimeSeriesMiniCubeManager aggs(String... timeSeries) { AGG_CONTEXT.set(timeSeries); return this; } private Set<String> cubeIds() { Set<String> cubeIds = new LinkedHashSet<String>(); String[] timeSeries = AGG_CONTEXT.get(); if (timeSeries == null || timeSeries.length == 0) { cubeIds.addAll(allCubeIds()); } else { for (String t : timeSeries) { cubeIds.addAll(cubeIds(t)); } if (cubeIds.isEmpty()) { throw new IllegalArgumentException("Can not find availd cubes for given time series " + ObjectUtils.getDisplayString(timeSeries)); } } LOGGER.info("Agg on cubes {}", ObjectUtils.getDisplayString(cubeIds)); return cubeIds; } private static abstract class Executee implements HazelcastInstanceAware, Serializable { /** * */ private static final long serialVersionUID = 1L; protected transient HazelcastInstance instance; protected transient TimeSeriesMiniCubeManagerHzImpl impl; public Executee() { super(); } @Override public void setHazelcastInstance(HazelcastInstance hazelcastInstance) { this.instance = hazelcastInstance; impl = (TimeSeriesMiniCubeManagerHzImpl) instance.getUserContext().get("this"); } } private static class Sum extends Executee implements Callable<BigDecimal> { /** * */ private static final long serialVersionUID = 1L; private String indName; private Map<String, List<Integer>> filterDims; public Sum(String indName, Map<String, List<Integer>> filterDims) { super(); this.indName = indName; this.filterDims = filterDims; } @Override public BigDecimal call() throws Exception { LOGGER.info("Sum on {}", instance.getCluster().getLocalMember()); return impl.miniCube == null ? new BigDecimal(0) : impl.miniCube.sum(indName, filterDims); } } @Override public BigDecimal sum(String indName) { return sum(indName, null); } @Override public BigDecimal sum(String indName, Map<String, List<Integer>> filterDims) { try { Set<String> cubeIds = cubeIds(); // Do execute List<BigDecimal> results = execute(new Sum(indName, filterDims), cubeIds, hzExecutorTimeout); LOGGER.debug("Sum {} on {} results is {}", indName, cubeIds, results); BigDecimal result = results.stream().reduce(new BigDecimal(0), (x, y) -> x.add(y)).setScale(IND_SCALE, BigDecimal.ROUND_HALF_UP); LOGGER.info("Sum {} on {} result is {}", indName, cubeIds, result); return result; } finally { AGG_CONTEXT.remove(); } } /** * FIXME: Need re-design * @author mengran * */ private static class Sum2 extends Executee implements Callable<Map<Integer, BigDecimal>> { /** * */ private static final long serialVersionUID = 1L; private String indName; private Map<String, List<Integer>> filterDims; private String groupDimName; public Sum2(String indName, String groupDimName, Map<String, List<Integer>> filterDims) { super(); this.indName = indName; this.filterDims = filterDims; this.groupDimName = groupDimName; } @Override public Map<Integer, BigDecimal> call() throws Exception { LOGGER.info("Sum on {}", instance.getCluster().getLocalMember()); return impl.miniCube == null ? null : impl.miniCube.sum(indName, groupDimName, filterDims); } } @Override public Map<Integer, BigDecimal> sum(String indName, String groupByDimName, Map<String, List<Integer>> filterDims) { try { Set<String> cubeIds = cubeIds(); // Do execute List<Map<Integer, BigDecimal>> results = execute(new Sum2(indName, groupByDimName, filterDims), cubeIds, hzExecutorTimeout); LOGGER.debug("Group {} on {} with filter {} results is {}", indName, cubeIds, filterDims, results); Map<Integer, BigDecimal> result = new HashMap<Integer, BigDecimal>(); results.stream().forEach(new Consumer<Map<Integer, BigDecimal>>() { @Override public void accept(Map<Integer, BigDecimal> t) { t.forEach((k, v) -> result.merge(k, v, BigDecimal::add)); } }); LOGGER.debug("Sum {} on {} with filter {} results is {}", indName, cubeIds, filterDims, result); return result; } finally { AGG_CONTEXT.remove(); } } private static class Count extends Executee implements Callable<Long> { /** * */ private static final long serialVersionUID = 1L; private String indName; private Map<String, List<Integer>> filterDims; public Count(String indName, Map<String, List<Integer>> filterDims) { super(); this.indName = indName; this.filterDims = filterDims; } @Override public Long call() throws Exception { LOGGER.info("Count on {}", instance.getCluster().getLocalMember()); return impl.miniCube == null ? 0L : impl.miniCube.count(indName, filterDims); } } @Override public long count(String indName) { return count(indName, null); } @Override public long count(String indName, Map<String, List<Integer>> filterDims) { try { Set<String> cubeIds = cubeIds(); // Do execute List<Long> results = execute(new Count(indName, filterDims), cubeIds, hzExecutorTimeout); LOGGER.debug("Count {} on {} results is {}", indName, cubeIds, results); Long result = results.stream().reduce(0L, (x, y) -> x + y); LOGGER.info("Count {} on {} result is {}", indName, cubeIds, result); return result; } finally { AGG_CONTEXT.remove(); } } /** * FIXME: Need re-design * @author mengran * */ private static class Count2 extends Executee implements Callable<Map<Integer, Long>> { /** * */ private static final long serialVersionUID = 1L; private String indName; private Map<String, List<Integer>> filterDims; private String groupDimName; public Count2(String indName, String groupDimName, Map<String, List<Integer>> filterDims) { super(); this.indName = indName; this.filterDims = filterDims; this.groupDimName = groupDimName; } @Override public Map<Integer, Long> call() throws Exception { LOGGER.info("Sum on {}", instance.getCluster().getLocalMember()); return impl.miniCube == null ? null : impl.miniCube.count(indName, groupDimName, filterDims); } } @Override public Map<Integer, Long> count(String indName, String groupByDimName, Map<String, List<Integer>> filterDims) { try { Set<String> cubeIds = cubeIds(); // Do execute List<Map<Integer, Long>> results = execute(new Count2(indName, groupByDimName, filterDims), cubeIds, hzExecutorTimeout); LOGGER.debug("Group counting {} on {} with filter {} results is {}", indName, cubeIds, filterDims, results); Map<Integer, Long> result = new HashMap<Integer, Long>(); results.stream().forEach(new Consumer<Map<Integer, Long>>() { @Override public void accept(Map<Integer, Long> t) { t.forEach((k, v) -> result.merge(k, v, Long::sum)); } }); LOGGER.debug("Count {} on {} with filter {} results is {}", indName, cubeIds, filterDims, result); return result; } finally { AGG_CONTEXT.remove(); } } /** * @author mengran * */ private static class Distinct extends Executee implements Callable<Map<Integer, RoaringBitmap>> { /** * */ private static final long serialVersionUID = 1L; private String indName; private Map<String, List<Integer>> filterDims; private String groupDimName; private boolean isDim; public Distinct(String indName, boolean isDim, String groupDimName, Map<String, List<Integer>> filterDims) { super(); this.indName = indName; this.filterDims = filterDims; this.groupDimName = groupDimName; this.isDim = isDim; } @Override public Map<Integer, RoaringBitmap> call() throws Exception { LOGGER.info("Distinct on {}", instance.getCluster().getLocalMember()); return impl.miniCube == null ? null : impl.miniCube.distinct(indName, isDim, groupDimName, filterDims); } } @Override public Map<Integer, RoaringBitmap> distinct(String distinctName, boolean isDim, String groupByDimName, Map<String, List<Integer>> filterDims) { try { Set<String> cubeIds = cubeIds(); // Do execute List<Map<Integer, RoaringBitmap>> results = execute( new Distinct(distinctName, isDim, groupByDimName, filterDims), cubeIds, hzExecutorTimeout); LOGGER.debug("Distinct {} on {} with filter {} results is {}", distinctName, cubeIds, filterDims, results); Map<Integer, RoaringBitmap> result = new HashMap<Integer, RoaringBitmap>(results.size()); results.stream().forEach(new Consumer<Map<Integer, RoaringBitmap>>() { @Override public void accept(Map<Integer, RoaringBitmap> t) { t.forEach((k, v) -> result.merge(k, v, new BiFunction<RoaringBitmap, RoaringBitmap, RoaringBitmap>() { @Override public RoaringBitmap apply(RoaringBitmap t, RoaringBitmap u) { return RoaringBitmap.or(t, u); } })); } }); LOGGER.debug("Distinct {} on {} with filter {} results is {}", distinctName, cubeIds, filterDims, result); return result; } finally { AGG_CONTEXT.remove(); } } @Override public Map<Integer, Integer> discnt(String distinctName, boolean isDim, String groupByDimName, Map<String, List<Integer>> filterDims) { try { Map<Integer, RoaringBitmap> distinct = distinct(distinctName, isDim, groupByDimName, filterDims); // Count it. Map<Integer, Integer> result = distinct.entrySet().stream() .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().getCardinality())); LOGGER.debug("Distinct {} on {} with filter {} results is {}", distinctName, AGG_CONTEXT.get(), filterDims, result); LOGGER.info("Distinct {} on {} with filter {} results size is {}", distinctName, AGG_CONTEXT.get(), filterDims, result.size()); return result; } finally { AGG_CONTEXT.remove(); } } }