/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.ignite.internal.processors.cache; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ThreadLocalRandom; import javax.cache.Cache; import org.apache.ignite.Ignite; import org.apache.ignite.IgniteCache; import org.apache.ignite.IgniteDataStreamer; import org.apache.ignite.IgniteException; import org.apache.ignite.binary.BinaryObject; import org.apache.ignite.cache.QueryEntity; import org.apache.ignite.cache.QueryIndex; import org.apache.ignite.cache.QueryIndexType; import org.apache.ignite.cache.affinity.Affinity; import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction; import org.apache.ignite.cache.query.QueryCursor; import org.apache.ignite.cache.query.ScanQuery; import org.apache.ignite.cache.query.SqlFieldsQuery; import org.apache.ignite.cluster.ClusterNode; import org.apache.ignite.configuration.CacheConfiguration; import org.apache.ignite.configuration.IgniteConfiguration; import org.apache.ignite.internal.util.typedef.G; import org.apache.ignite.lang.IgniteCallable; import org.apache.ignite.lang.IgniteRunnable; import org.apache.ignite.resources.IgniteInstanceResource; import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi; import org.apache.ignite.spi.discovery.tcp.ipfinder.TcpDiscoveryIpFinder; import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder; import org.apache.ignite.testframework.GridTestUtils; import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; import static org.apache.ignite.cache.CacheAtomicityMode.TRANSACTIONAL; import static org.apache.ignite.cache.CacheMode.PARTITIONED; import static org.apache.ignite.cache.CacheWriteSynchronizationMode.FULL_SYNC; import static org.apache.ignite.transactions.TransactionConcurrency.PESSIMISTIC; import static org.apache.ignite.transactions.TransactionIsolation.REPEATABLE_READ; /** * */ @SuppressWarnings("unchecked") public class IgniteCacheQueriesLoadTest1 extends GridCommonAbstractTest { /** Operation. */ private static final String OPERATION = "Operation"; /** Deposit. */ private static final String DEPOSIT = "Deposit"; /** Trader. */ private static final String TRADER = "Trader"; /** Id. */ private static final String ID = "ID"; /** Deposit id. */ private static final String DEPOSIT_ID = "DEPOSIT_ID"; /** Trader id. */ private static final String TRADER_ID = "TRADER_ID"; /** Firstname. */ private static final String FIRSTNAME = "FIRSTNAME"; /** Secondname. */ private static final String SECONDNAME = "SECONDNAME"; /** Email. */ private static final String EMAIL = "EMAIL"; /** Business day. */ private static final String BUSINESS_DAY = "BUSINESS_DAY"; /** Trader link. */ private static final String TRADER_LINK = "TRADER"; /** Balance. */ private static final String BALANCE = "BALANCE"; /** Margin rate. */ private static final String MARGIN_RATE = "MARGIN_RATE"; /** Balance on day open. */ private static final String BALANCE_ON_DAY_OPEN = "BALANCEDO"; /** Trader cache name. */ private static final String TRADER_CACHE = "TRADER_CACHE"; /** Deposit cache name. */ private static final String DEPOSIT_CACHE = "DEPOSIT_CACHE"; /** History of operation over deposit. */ private static final String DEPOSIT_HISTORY_CACHE = "DEPOSIT_HISTORY_CACHE"; /** Count of operations by deposit. */ private static final String DEPOSIT_OPERATION_COUNT_SQL = "SELECT COUNT(*) FROM \"" + DEPOSIT_HISTORY_CACHE + "\"." + OPERATION + " WHERE " + "DEPOSIT_ID" + "=?"; /** Get last history row. */ private static final String LAST_HISTORY_ROW_SQL = "SELECT MAX("+BUSINESS_DAY+") FROM \""+DEPOSIT_HISTORY_CACHE + "\"." + OPERATION + " WHERE " + "DEPOSIT_ID" + "=?"; /** Find deposit SQL query. */ private static final String FIND_DEPOSIT_SQL = "SELECT _key FROM \"" + DEPOSIT_CACHE + "\"." + DEPOSIT + " WHERE " + TRADER_ID + "=?"; /** */ private static final TcpDiscoveryIpFinder IP_FINDER = new TcpDiscoveryVmIpFinder(true); /** */ private static final int NODES = 5; /** Distribution of partitions by nodes. */ private Map<UUID, List<Integer>> partitionsMap; /** Preload amount. */ private final int preloadAmount = 10_000; /** {@inheritDoc} */ @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception { IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName); cfg.setIncludeEventTypes(); cfg.setMarshaller(null); ((TcpDiscoverySpi)cfg.getDiscoverySpi()).setIpFinder(IP_FINDER); RendezvousAffinityFunction aff = new RendezvousAffinityFunction(); aff.setPartitions(3000); CacheConfiguration<Object, Object> parentCfg = new CacheConfiguration<>(DEFAULT_CACHE_NAME); parentCfg.setAffinity(aff); parentCfg.setAtomicityMode(TRANSACTIONAL); parentCfg.setCacheMode(PARTITIONED); parentCfg.setBackups(2); parentCfg.setWriteSynchronizationMode(FULL_SYNC); cfg.setCacheConfiguration( getTraderCfg(parentCfg), getDepositCfg(parentCfg), getDepositHistoryCfg(parentCfg) ); return cfg; } /** * @throws Exception If failed. */ public void testQueries() throws Exception { runQueries(1, true, 10_000); runQueries(10, false, 30_000); } /** * @param threads Threads number. * @param checkBalance Check balance flag. * @param time Execution time. * @throws Exception If failed. */ private void runQueries(int threads, final boolean checkBalance, final long time) throws Exception { final Ignite ignite = grid(0); GridTestUtils.runMultiThreaded(new Callable<Object>() { @Override public Object call() { long endTime = System.currentTimeMillis() + time; while (System.currentTimeMillis() < endTime) { ScanQueryBroadcastClosure c = new ScanQueryBroadcastClosure(partitionsMap, checkBalance); ignite.compute().broadcast(c); } return null; } }, threads, "test-thread"); } /** {@inheritDoc} */ @Override protected void beforeTestsStarted() throws Exception { startGridsMultiThreaded(NODES); partitionsMap = traderCachePartitions(ignite(0)); assertEquals(NODES, partitionsMap.size()); preLoading(); } /** {@inheritDoc} */ @Override protected void afterTestsStopped() throws Exception { stopAllGrids(); assert G.allGrids().isEmpty(); } /** * @throws Exception If fail. */ private void preLoading() throws Exception { final Thread preloadAccount = new Thread() { @Override public void run() { setName("preloadTraders"); Ignite ignite = ignite(0); try (IgniteDataStreamer dataLdr = ignite.dataStreamer(TRADER_CACHE)) { for (int i = 0; i < preloadAmount && !isInterrupted(); i++) { String traderKey = "traderId=" + i; dataLdr.addData(traderKey, createTrader(ignite, traderKey)); } } } }; preloadAccount.start(); Thread preloadTrade = new Thread() { @Override public void run() { setName("preloadDeposits"); Ignite ignite = ignite(0); try (IgniteDataStreamer dataLdr = ignite.dataStreamer(DEPOSIT_CACHE)) { for (int i = 0; i < preloadAmount && !isInterrupted(); i++) { int traderId = nextRandom(preloadAmount); String traderKey = "traderId=" + traderId; String key = traderKey + "&depositId=" + i; dataLdr.addData(key, createDeposit(ignite, key, traderKey, i)); } } } }; preloadTrade.start(); preloadTrade.join(); preloadAccount.join(); } /** * @param ignite Node. * @param id Identifier. * @return Trader entity as binary object. */ private BinaryObject createTrader(Ignite ignite, String id) { return ignite.binary() .builder(TRADER) .setField(ID, id) .setField(FIRSTNAME, "First name " + id) .setField(SECONDNAME, "Second name " + id) .setField(EMAIL, "trader" + id + "@mail.org") .build(); } /** * @param ignite Node. * @param id Identifier. * @param traderId Key. * @param num Num. * @return Deposit entity as binary object. */ private BinaryObject createDeposit(Ignite ignite, String id, String traderId, int num) { double startBalance = 100 + nextRandom(100) / 1.123; return ignite.binary() .builder(DEPOSIT) .setField(ID, id) .setField(TRADER_ID, traderId) .setField(TRADER_LINK, num) .setField(BALANCE, new BigDecimal(startBalance)) .setField(MARGIN_RATE, new BigDecimal(0.1)) .setField(BALANCE_ON_DAY_OPEN, new BigDecimal(startBalance)) .build(); } /** * Building a map that contains mapping of node ID to a list of partitions stored on the node. * * @param ignite Node. * @return Node to partitions map. */ private Map<UUID, List<Integer>> traderCachePartitions(Ignite ignite) { // Getting affinity for account cache. Affinity<?> affinity = ignite.affinity(TRADER_CACHE); // Building a list of all partitions numbers. List<Integer> partNumbers = new ArrayList<>(affinity.partitions()); for (int i = 0; i < affinity.partitions(); i++) partNumbers.add(i); // Getting partition to node mapping. Map<Integer, ClusterNode> partPerNodes = affinity.mapPartitionsToNodes(partNumbers); // Building node to partitions mapping. Map<UUID, List<Integer>> nodesToPart = new HashMap<>(); for (Map.Entry<Integer, ClusterNode> entry : partPerNodes.entrySet()) { List<Integer> nodeParts = nodesToPart.get(entry.getValue().id()); if (nodeParts == null) { nodeParts = new ArrayList<>(); nodesToPart.put(entry.getValue().id(), nodeParts); } nodeParts.add(entry.getKey()); } return nodesToPart; } /** * Closure for scan query executing. */ private static class ScanQueryBroadcastClosure implements IgniteRunnable { /** * Ignite node. */ @IgniteInstanceResource private Ignite node; /** * Information about partition. */ private final Map<UUID, List<Integer>> cachePart; /** */ private final boolean checkBalance; /** * @param cachePart Partition by node for Ignite cache. * @param checkBalance Check balance flag. */ private ScanQueryBroadcastClosure(Map<UUID, List<Integer>> cachePart, boolean checkBalance) { this.cachePart = cachePart; this.checkBalance = checkBalance; } /** {@inheritDoc} */ @Override public void run() { try { IgniteCache traders = node.cache(TRADER_CACHE).withKeepBinary(); IgniteCache<String, BinaryObject> depositCache = node.cache(DEPOSIT_CACHE).withKeepBinary(); // Getting a list of the partitions owned by this node. List<Integer> myPartitions = cachePart.get(node.cluster().localNode().id()); for (Integer part : myPartitions) { ScanQuery scanQry = new ScanQuery(); scanQry.setPartition(part); try (QueryCursor<Cache.Entry<String, BinaryObject>> cursor = traders.query(scanQry)) { for (Cache.Entry<String, BinaryObject> entry : cursor) { String traderId = entry.getKey(); SqlFieldsQuery findDepositQry = new SqlFieldsQuery(FIND_DEPOSIT_SQL).setLocal(true); try (QueryCursor cursor1 = depositCache.query(findDepositQry.setArgs(traderId))) { for (Object obj : cursor1) { List<String> depositIds = (List<String>)obj; for (String depositId : depositIds) { updateDeposit(depositCache, depositId); checkDeposit(depositCache, depositId); } } } } } } } catch (Exception e) { throw new IgniteException(e); } } /** * @param depositCache Ignite cache of deposit. * @param depositKey Key of deposit. * @throws Exception If failed. */ private void updateDeposit(final IgniteCache<String, BinaryObject> depositCache, final String depositKey) throws Exception { final IgniteCache histCache = node.cache(DEPOSIT_HISTORY_CACHE).withKeepBinary(); doInTransaction(node, PESSIMISTIC, REPEATABLE_READ, new IgniteCallable<Object>() { @Override public Object call() throws Exception { BinaryObject deposit = depositCache.get(depositKey); BigDecimal amount = deposit.field(BALANCE); BigDecimal rate = deposit.field(MARGIN_RATE); BigDecimal newBalance = amount.multiply(rate.add(BigDecimal.ONE)); deposit = deposit.toBuilder() .setField(BALANCE, newBalance) .build(); SqlFieldsQuery findDepositHist = new SqlFieldsQuery(LAST_HISTORY_ROW_SQL).setLocal(true); try (QueryCursor cursor1 = histCache.query(findDepositHist.setArgs(depositKey))) { for (Object o: cursor1){ // No-op. } } String depositHistKey = depositKey + "&histId=" + System.nanoTime(); BinaryObject depositHistRow = node.binary().builder(OPERATION) .setField(ID, depositHistKey) .setField(DEPOSIT_ID, depositKey) .setField(BUSINESS_DAY, new Date()) .setField(BALANCE, newBalance) .build(); histCache.put(depositHistKey, depositHistRow); depositCache.put(depositKey, deposit); return null; } }); } /** * @param depositCache Deposit cache. * @param depositKey Deposit key. */ private void checkDeposit(IgniteCache<String, BinaryObject> depositCache, String depositKey) { IgniteCache histCache = node.cache(DEPOSIT_HISTORY_CACHE).withKeepBinary(); BinaryObject deposit = depositCache.get(depositKey); BigDecimal startBalance = deposit.field(BALANCE_ON_DAY_OPEN); BigDecimal balance = deposit.field(BALANCE); BigDecimal rate = deposit.field(MARGIN_RATE); BigDecimal expBalance; SqlFieldsQuery findDepositHist = new SqlFieldsQuery(DEPOSIT_OPERATION_COUNT_SQL); try (QueryCursor cursor1 = histCache.query(findDepositHist.setArgs(depositKey))) { Long cnt = (Long)((ArrayList)cursor1.iterator().next()).get(0); expBalance = startBalance.multiply(rate.add(BigDecimal.ONE).pow(cnt.intValue())); } expBalance = expBalance.setScale(2, BigDecimal.ROUND_DOWN); balance = balance.setScale(2, BigDecimal.ROUND_DOWN); if (checkBalance && !expBalance.equals(balance)) { node.log().error("Deposit " + depositKey + " has incorrect balance " + balance + " when expected " + expBalance, null); throw new IgniteException("Deposit " + depositKey + " has incorrect balance " + balance + " when expected " + expBalance); } } } /** * @param max Max. * @return Random value. */ private static int nextRandom(int max) { return ThreadLocalRandom.current().nextInt(max); } /** * @param parentCfg Parent config. * @return Configuration. */ private static CacheConfiguration<Object, Object> getDepositHistoryCfg( CacheConfiguration<Object, Object> parentCfg) { CacheConfiguration<Object, Object> depositHistCfg = new CacheConfiguration<>(parentCfg); depositHistCfg.setName(DEPOSIT_HISTORY_CACHE); String strCls = String.class.getCanonicalName(); String dblCls = Double.class.getCanonicalName(); String dtCls = Date.class.getCanonicalName(); LinkedHashMap<String, String> qryFields = new LinkedHashMap<>(); qryFields.put(ID, strCls); qryFields.put(DEPOSIT_ID, strCls); qryFields.put(BUSINESS_DAY, dtCls); qryFields.put(BALANCE, dblCls); QueryEntity qryEntity = new QueryEntity(); qryEntity.setValueType(OPERATION); qryEntity.setKeyType(strCls); qryEntity.setFields(qryFields); qryEntity.setIndexes(Arrays.asList(new QueryIndex(ID, true), new QueryIndex(DEPOSIT_ID, true))); depositHistCfg.setQueryEntities(Collections.singleton(qryEntity)); return depositHistCfg; } /** * @param parentCfg Parent config. * @return Configuration. */ private static CacheConfiguration<Object, Object> getDepositCfg(CacheConfiguration<Object, Object> parentCfg) { CacheConfiguration<Object, Object> depositCfg = new CacheConfiguration<>(parentCfg); depositCfg.setName(DEPOSIT_CACHE); String strCls = String.class.getCanonicalName(); String dblCls = Double.class.getCanonicalName(); String intCls = Integer.class.getCanonicalName(); LinkedHashMap<String, String> qryFields = new LinkedHashMap<>(); qryFields.put(ID, strCls); qryFields.put(TRADER_ID, strCls); qryFields.put(TRADER_LINK, intCls); qryFields.put(BALANCE, dblCls); qryFields.put(MARGIN_RATE, dblCls); qryFields.put(BALANCE_ON_DAY_OPEN, dblCls); QueryEntity qryEntity = new QueryEntity(); qryEntity.setValueType(DEPOSIT); qryEntity.setKeyType(strCls); qryEntity.setFields(qryFields); qryEntity.setIndexes(Collections.singleton(new QueryIndex(ID, false))); depositCfg.setQueryEntities(Collections.singleton(qryEntity)); return depositCfg; } /** * @param parentCfg Parent config. * @return Configuration. */ private static CacheConfiguration<Object, Object> getTraderCfg(CacheConfiguration<Object, Object> parentCfg) { CacheConfiguration<Object, Object> traderCfg = new CacheConfiguration<>(parentCfg); traderCfg.setName(TRADER_CACHE); String strCls = String.class.getCanonicalName(); LinkedHashMap<String, String> qryFields = new LinkedHashMap<>(); qryFields.put(ID, strCls); qryFields.put(FIRSTNAME, strCls); qryFields.put(SECONDNAME, strCls); qryFields.put(EMAIL, strCls); QueryEntity qryEntity = new QueryEntity(); qryEntity.setValueType(TRADER); qryEntity.setKeyType(strCls); qryEntity.setFields(qryFields); LinkedHashMap<String, Boolean> grpIdx = new LinkedHashMap<>(); grpIdx.put(FIRSTNAME, false); grpIdx.put(SECONDNAME, false); qryEntity.setIndexes(Arrays.asList( new QueryIndex(ID, true), new QueryIndex(grpIdx, QueryIndexType.FULLTEXT) )); traderCfg.setQueryEntities(Collections.singleton(qryEntity)); return traderCfg; } }