| /* |
| * |
| * 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.cassandra.stress; |
| |
| |
| import java.io.IOError; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.Serializable; |
| import java.net.URI; |
| import java.util.*; |
| import java.util.concurrent.TimeUnit; |
| |
| import com.google.common.base.Function; |
| import com.google.common.util.concurrent.Uninterruptibles; |
| |
| import com.datastax.driver.core.*; |
| import com.datastax.driver.core.exceptions.AlreadyExistsException; |
| import org.apache.cassandra.config.CFMetaData; |
| import org.apache.cassandra.cql3.QueryProcessor; |
| import org.apache.cassandra.cql3.statements.CreateKeyspaceStatement; |
| import org.apache.cassandra.exceptions.RequestValidationException; |
| import org.apache.cassandra.exceptions.SyntaxException; |
| import org.apache.cassandra.stress.generate.*; |
| import org.apache.cassandra.stress.generate.values.*; |
| import org.apache.cassandra.stress.operations.userdefined.TokenRangeQuery; |
| import org.apache.cassandra.stress.operations.userdefined.SchemaInsert; |
| import org.apache.cassandra.stress.operations.userdefined.SchemaQuery; |
| import org.apache.cassandra.stress.operations.userdefined.ValidatingSchemaQuery; |
| import org.apache.cassandra.stress.settings.*; |
| import org.apache.cassandra.stress.util.JavaDriverClient; |
| import org.apache.cassandra.stress.util.ThriftClient; |
| import org.apache.cassandra.stress.util.Timer; |
| import org.apache.cassandra.thrift.Compression; |
| import org.apache.cassandra.thrift.ThriftConversion; |
| import org.apache.thrift.TException; |
| import org.yaml.snakeyaml.Yaml; |
| import org.yaml.snakeyaml.constructor.Constructor; |
| import org.yaml.snakeyaml.error.YAMLException; |
| |
| public class StressProfile implements Serializable |
| { |
| private String keyspaceCql; |
| private String tableCql; |
| private List<String> extraSchemaDefinitions; |
| private String seedStr; |
| |
| public String keyspaceName; |
| public String tableName; |
| private Map<String, GeneratorConfig> columnConfigs; |
| private Map<String, StressYaml.QueryDef> queries; |
| public Map<String, StressYaml.TokenRangeQueryDef> tokenRangeQueries; |
| private Map<String, String> insert; |
| |
| transient volatile TableMetadata tableMetaData; |
| transient volatile Set<TokenRange> tokenRanges; |
| |
| transient volatile GeneratorFactory generatorFactory; |
| |
| transient volatile BatchStatement.Type batchType; |
| transient volatile DistributionFactory partitions; |
| transient volatile RatioDistributionFactory selectchance; |
| transient volatile RatioDistributionFactory rowPopulation; |
| transient volatile PreparedStatement insertStatement; |
| transient volatile Integer thriftInsertId; |
| transient volatile List<ValidatingSchemaQuery.Factory> validationFactories; |
| |
| transient volatile Map<String, SchemaQuery.ArgSelect> argSelects; |
| transient volatile Map<String, PreparedStatement> queryStatements; |
| transient volatile Map<String, Integer> thriftQueryIds; |
| |
| private void init(StressYaml yaml) throws RequestValidationException |
| { |
| keyspaceName = yaml.keyspace; |
| keyspaceCql = yaml.keyspace_definition; |
| tableName = yaml.table; |
| tableCql = yaml.table_definition; |
| seedStr = "seed for stress"; |
| queries = yaml.queries; |
| tokenRangeQueries = yaml.token_range_queries; |
| insert = yaml.insert; |
| |
| extraSchemaDefinitions = yaml.extra_definitions; |
| |
| assert keyspaceName != null : "keyspace name is required in yaml file"; |
| assert tableName != null : "table name is required in yaml file"; |
| assert queries != null : "queries map is required in yaml file"; |
| |
| for (String query : queries.keySet()) |
| assert !tokenRangeQueries.containsKey(query) : |
| String.format("Found %s in both queries and token_range_queries, please use different names", query); |
| |
| if (keyspaceCql != null && keyspaceCql.length() > 0) |
| { |
| try |
| { |
| String name = ((CreateKeyspaceStatement) QueryProcessor.parseStatement(keyspaceCql)).keyspace(); |
| assert name.equalsIgnoreCase(keyspaceName) : "Name in keyspace_definition doesn't match keyspace property: '" + name + "' != '" + keyspaceName + "'"; |
| } |
| catch (SyntaxException e) |
| { |
| throw new IllegalArgumentException("There was a problem parsing the keyspace cql: " + e.getMessage()); |
| } |
| } |
| else |
| { |
| keyspaceCql = null; |
| } |
| |
| if (tableCql != null && tableCql.length() > 0) |
| { |
| try |
| { |
| String name = CFMetaData.compile(tableCql, keyspaceName).cfName; |
| assert name.equalsIgnoreCase(tableName) : "Name in table_definition doesn't match table property: '" + name + "' != '" + tableName + "'"; |
| } |
| catch (RuntimeException e) |
| { |
| throw new IllegalArgumentException("There was a problem parsing the table cql: " + e.getMessage()); |
| } |
| } |
| else |
| { |
| tableCql = null; |
| } |
| |
| columnConfigs = new HashMap<>(); |
| |
| if (yaml.columnspec != null) |
| { |
| for (Map<String, Object> spec : yaml.columnspec) |
| { |
| lowerCase(spec); |
| String name = (String) spec.remove("name"); |
| DistributionFactory population = !spec.containsKey("population") ? null : OptionDistribution.get((String) spec.remove("population")); |
| DistributionFactory size = !spec.containsKey("size") ? null : OptionDistribution.get((String) spec.remove("size")); |
| DistributionFactory clustering = !spec.containsKey("cluster") ? null : OptionDistribution.get((String) spec.remove("cluster")); |
| |
| if (!spec.isEmpty()) |
| throw new IllegalArgumentException("Unrecognised option(s) in column spec: " + spec); |
| if (name == null) |
| throw new IllegalArgumentException("Missing name argument in column spec"); |
| |
| GeneratorConfig config = new GeneratorConfig(seedStr + name, clustering, size, population); |
| columnConfigs.put(name, config); |
| } |
| } |
| } |
| |
| public void maybeCreateSchema(StressSettings settings) |
| { |
| JavaDriverClient client = settings.getJavaDriverClient(false); |
| |
| if (keyspaceCql != null) |
| { |
| try |
| { |
| client.execute(keyspaceCql, org.apache.cassandra.db.ConsistencyLevel.ONE); |
| } |
| catch (AlreadyExistsException e) |
| { |
| } |
| } |
| |
| client.execute("use " + keyspaceName, org.apache.cassandra.db.ConsistencyLevel.ONE); |
| |
| if (tableCql != null) |
| { |
| try |
| { |
| client.execute(tableCql, org.apache.cassandra.db.ConsistencyLevel.ONE); |
| } |
| catch (AlreadyExistsException e) |
| { |
| } |
| |
| System.out.println(String.format("Created schema. Sleeping %ss for propagation.", settings.node.nodes.size())); |
| Uninterruptibles.sleepUninterruptibly(settings.node.nodes.size(), TimeUnit.SECONDS); |
| } |
| |
| if (extraSchemaDefinitions != null) |
| { |
| for (String extraCql : extraSchemaDefinitions) |
| { |
| |
| try |
| { |
| client.execute(extraCql, org.apache.cassandra.db.ConsistencyLevel.ONE); |
| } |
| catch (AlreadyExistsException e) |
| { |
| } |
| } |
| |
| System.out.println(String.format("Created extra schema. Sleeping %ss for propagation.", settings.node.nodes.size())); |
| Uninterruptibles.sleepUninterruptibly(settings.node.nodes.size(), TimeUnit.SECONDS); |
| } |
| |
| maybeLoadSchemaInfo(settings); |
| } |
| |
| public void truncateTable(StressSettings settings) |
| { |
| JavaDriverClient client = settings.getJavaDriverClient(false); |
| assert settings.command.truncate != SettingsCommand.TruncateWhen.NEVER; |
| String cql = String.format("TRUNCATE %s.%s", keyspaceName, tableName); |
| client.execute(cql, org.apache.cassandra.db.ConsistencyLevel.ONE); |
| System.out.println(String.format("Truncated %s.%s. Sleeping %ss for propagation.", |
| keyspaceName, tableName, settings.node.nodes.size())); |
| Uninterruptibles.sleepUninterruptibly(settings.node.nodes.size(), TimeUnit.SECONDS); |
| } |
| |
| private void maybeLoadSchemaInfo(StressSettings settings) |
| { |
| if (tableMetaData == null) |
| { |
| JavaDriverClient client = settings.getJavaDriverClient(); |
| |
| synchronized (client) |
| { |
| |
| if (tableMetaData != null) |
| return; |
| |
| TableMetadata metadata = client.getCluster() |
| .getMetadata() |
| .getKeyspace(keyspaceName) |
| .getTable(tableName); |
| |
| if (metadata == null) |
| throw new RuntimeException("Unable to find table " + keyspaceName + "." + tableName); |
| |
| //Fill in missing column configs |
| for (ColumnMetadata col : metadata.getColumns()) |
| { |
| if (columnConfigs.containsKey(col.getName())) |
| continue; |
| |
| columnConfigs.put(col.getName(), new GeneratorConfig(seedStr + col.getName(), null, null, null)); |
| } |
| |
| tableMetaData = metadata; |
| } |
| } |
| } |
| |
| public Set<TokenRange> maybeLoadTokenRanges(StressSettings settings) |
| { |
| maybeLoadSchemaInfo(settings); // ensure table metadata is available |
| |
| JavaDriverClient client = settings.getJavaDriverClient(); |
| synchronized (client) |
| { |
| if (tokenRanges != null) |
| return tokenRanges; |
| |
| Cluster cluster = client.getCluster(); |
| Metadata metadata = cluster.getMetadata(); |
| if (metadata == null) |
| throw new RuntimeException("Unable to get metadata"); |
| |
| List<TokenRange> sortedRanges = new ArrayList<>(metadata.getTokenRanges().size() + 1); |
| for (TokenRange range : metadata.getTokenRanges()) |
| { |
| //if we don't unwrap we miss the partitions between ring min and smallest range start value |
| if (range.isWrappedAround()) |
| sortedRanges.addAll(range.unwrap()); |
| else |
| sortedRanges.add(range); |
| } |
| |
| Collections.sort(sortedRanges); |
| tokenRanges = new LinkedHashSet<>(sortedRanges); |
| return tokenRanges; |
| } |
| } |
| |
| public Operation getQuery(String name, |
| Timer timer, |
| PartitionGenerator generator, |
| SeedManager seeds, |
| StressSettings settings, |
| boolean isWarmup) |
| { |
| name = name.toLowerCase(); |
| if (!queries.containsKey(name)) |
| throw new IllegalArgumentException("No query defined with name " + name); |
| |
| if (queryStatements == null) |
| { |
| synchronized (this) |
| { |
| if (queryStatements == null) |
| { |
| try |
| { |
| JavaDriverClient jclient = settings.getJavaDriverClient(); |
| ThriftClient tclient = null; |
| |
| if (settings.mode.api != ConnectionAPI.JAVA_DRIVER_NATIVE) |
| tclient = settings.getThriftClient(); |
| |
| Map<String, PreparedStatement> stmts = new HashMap<>(); |
| Map<String, Integer> tids = new HashMap<>(); |
| Map<String, SchemaQuery.ArgSelect> args = new HashMap<>(); |
| for (Map.Entry<String, StressYaml.QueryDef> e : queries.entrySet()) |
| { |
| stmts.put(e.getKey().toLowerCase(), jclient.prepare(e.getValue().cql)); |
| |
| if (tclient != null) |
| tids.put(e.getKey().toLowerCase(), tclient.prepare_cql3_query(e.getValue().cql, Compression.NONE)); |
| |
| args.put(e.getKey().toLowerCase(), e.getValue().fields == null |
| ? SchemaQuery.ArgSelect.MULTIROW |
| : SchemaQuery.ArgSelect.valueOf(e.getValue().fields.toUpperCase())); |
| } |
| thriftQueryIds = tids; |
| queryStatements = stmts; |
| argSelects = args; |
| } |
| catch (TException e) |
| { |
| throw new RuntimeException(e); |
| } |
| } |
| } |
| } |
| |
| return new SchemaQuery(timer, settings, generator, seeds, thriftQueryIds.get(name), queryStatements.get(name), |
| ThriftConversion.fromThrift(settings.command.consistencyLevel), argSelects.get(name)); |
| } |
| |
| public Operation getBulkReadQueries(String name, Timer timer, StressSettings settings, TokenRangeIterator tokenRangeIterator, boolean isWarmup) |
| { |
| StressYaml.TokenRangeQueryDef def = tokenRangeQueries.get(name); |
| if (def == null) |
| throw new IllegalArgumentException("No bulk read query defined with name " + name); |
| |
| return new TokenRangeQuery(timer, settings, tableMetaData, tokenRangeIterator, def, isWarmup); |
| } |
| |
| public SchemaInsert getInsert(Timer timer, PartitionGenerator generator, SeedManager seedManager, StressSettings settings) |
| { |
| if (insertStatement == null) |
| { |
| synchronized (this) |
| { |
| if (insertStatement == null) |
| { |
| maybeLoadSchemaInfo(settings); |
| |
| Set<ColumnMetadata> keyColumns = com.google.common.collect.Sets.newHashSet(tableMetaData.getPrimaryKey()); |
| |
| //Non PK Columns |
| StringBuilder sb = new StringBuilder(); |
| |
| sb.append("UPDATE \"").append(tableName).append("\" SET "); |
| |
| //PK Columns |
| StringBuilder pred = new StringBuilder(); |
| pred.append(" WHERE "); |
| |
| boolean firstCol = true; |
| boolean firstPred = true; |
| for (ColumnMetadata c : tableMetaData.getColumns()) |
| { |
| |
| if (keyColumns.contains(c)) |
| { |
| if (firstPred) |
| firstPred = false; |
| else |
| pred.append(" AND "); |
| |
| pred.append(c.getName()).append(" = ?"); |
| } |
| else |
| { |
| if (firstCol) |
| firstCol = false; |
| else |
| sb.append(","); |
| |
| sb.append(c.getName()).append(" = "); |
| |
| switch (c.getType().getName()) |
| { |
| case SET: |
| case LIST: |
| case COUNTER: |
| sb.append(c.getName()).append(" + ?"); |
| break; |
| default: |
| sb.append("?"); |
| break; |
| } |
| } |
| } |
| |
| //Put PK predicates at the end |
| sb.append(pred); |
| |
| if (insert == null) |
| insert = new HashMap<>(); |
| lowerCase(insert); |
| |
| partitions = select(settings.insert.batchsize, "partitions", "fixed(1)", insert, OptionDistribution.BUILDER); |
| selectchance = select(settings.insert.selectRatio, "select", "fixed(1)/1", insert, OptionRatioDistribution.BUILDER); |
| rowPopulation = select(settings.insert.rowPopulationRatio, "row-population", "fixed(1)/1", insert, OptionRatioDistribution.BUILDER); |
| batchType = settings.insert.batchType != null |
| ? settings.insert.batchType |
| : !insert.containsKey("batchtype") |
| ? BatchStatement.Type.LOGGED |
| : BatchStatement.Type.valueOf(insert.remove("batchtype")); |
| if (!insert.isEmpty()) |
| throw new IllegalArgumentException("Unrecognised insert option(s): " + insert); |
| |
| Distribution visits = settings.insert.visits.get(); |
| // these min/max are not absolutely accurate if selectchance < 1, but they're close enough to |
| // guarantee the vast majority of actions occur in these bounds |
| double minBatchSize = selectchance.get().min() * partitions.get().minValue() * generator.minRowCount * (1d / visits.maxValue()); |
| double maxBatchSize = selectchance.get().max() * partitions.get().maxValue() * generator.maxRowCount * (1d / visits.minValue()); |
| System.out.printf("Generating batches with [%d..%d] partitions and [%.0f..%.0f] rows (of [%.0f..%.0f] total rows in the partitions)%n", |
| partitions.get().minValue(), partitions.get().maxValue(), |
| minBatchSize, maxBatchSize, |
| partitions.get().minValue() * generator.minRowCount, |
| partitions.get().maxValue() * generator.maxRowCount); |
| if (generator.maxRowCount > 100 * 1000 * 1000) |
| System.err.printf("WARNING: You have defined a schema that permits very large partitions (%.0f max rows (>100M))%n", generator.maxRowCount); |
| if (batchType == BatchStatement.Type.LOGGED && maxBatchSize > 65535) |
| { |
| System.err.printf("ERROR: You have defined a workload that generates batches with more than 65k rows (%.0f), but have required the use of LOGGED batches. There is a 65k row limit on a single batch.%n", |
| selectchance.get().max() * partitions.get().maxValue() * generator.maxRowCount); |
| System.exit(1); |
| } |
| if (maxBatchSize > 100000) |
| System.err.printf("WARNING: You have defined a schema that permits very large batches (%.0f max rows (>100K)). This may OOM this stress client, or the server.%n", |
| selectchance.get().max() * partitions.get().maxValue() * generator.maxRowCount); |
| |
| JavaDriverClient client = settings.getJavaDriverClient(); |
| String query = sb.toString(); |
| |
| if (settings.mode.api != ConnectionAPI.JAVA_DRIVER_NATIVE) |
| { |
| try |
| { |
| thriftInsertId = settings.getThriftClient().prepare_cql3_query(query, Compression.NONE); |
| } |
| catch (TException e) |
| { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| insertStatement = client.prepare(query); |
| } |
| } |
| } |
| |
| return new SchemaInsert(timer, settings, generator, seedManager, partitions.get(), selectchance.get(), rowPopulation.get(), thriftInsertId, insertStatement, ThriftConversion.fromThrift(settings.command.consistencyLevel), batchType); |
| } |
| |
| public List<ValidatingSchemaQuery> getValidate(Timer timer, PartitionGenerator generator, SeedManager seedManager, StressSettings settings) |
| { |
| if (validationFactories == null) |
| { |
| synchronized (this) |
| { |
| if (validationFactories == null) |
| { |
| maybeLoadSchemaInfo(settings); |
| validationFactories = ValidatingSchemaQuery.create(tableMetaData, settings); |
| } |
| } |
| } |
| |
| List<ValidatingSchemaQuery> queries = new ArrayList<>(); |
| for (ValidatingSchemaQuery.Factory factory : validationFactories) |
| queries.add(factory.create(timer, settings, generator, seedManager, ThriftConversion.fromThrift(settings.command.consistencyLevel))); |
| return queries; |
| } |
| |
| private static <E> E select(E first, String key, String defValue, Map<String, String> map, Function<String, E> builder) |
| { |
| String val = map.remove(key); |
| |
| if (first != null) |
| return first; |
| if (val != null && val.trim().length() > 0) |
| return builder.apply(val); |
| |
| return builder.apply(defValue); |
| } |
| |
| public PartitionGenerator newGenerator(StressSettings settings) |
| { |
| if (generatorFactory == null) |
| { |
| synchronized (this) |
| { |
| maybeLoadSchemaInfo(settings); |
| if (generatorFactory == null) |
| generatorFactory = new GeneratorFactory(); |
| } |
| } |
| |
| return generatorFactory.newGenerator(settings); |
| } |
| |
| private class GeneratorFactory |
| { |
| final List<ColumnInfo> partitionKeys = new ArrayList<>(); |
| final List<ColumnInfo> clusteringColumns = new ArrayList<>(); |
| final List<ColumnInfo> valueColumns = new ArrayList<>(); |
| |
| private GeneratorFactory() |
| { |
| Set<ColumnMetadata> keyColumns = com.google.common.collect.Sets.newHashSet(tableMetaData.getPrimaryKey()); |
| |
| for (ColumnMetadata metadata : tableMetaData.getPartitionKey()) |
| partitionKeys.add(new ColumnInfo(metadata.getName(), metadata.getType(), columnConfigs.get(metadata.getName()))); |
| for (ColumnMetadata metadata : tableMetaData.getClusteringColumns()) |
| clusteringColumns.add(new ColumnInfo(metadata.getName(), metadata.getType(), columnConfigs.get(metadata.getName()))); |
| for (ColumnMetadata metadata : tableMetaData.getColumns()) |
| if (!keyColumns.contains(metadata)) |
| valueColumns.add(new ColumnInfo(metadata.getName(), metadata.getType(), columnConfigs.get(metadata.getName()))); |
| } |
| |
| PartitionGenerator newGenerator(StressSettings settings) |
| { |
| return new PartitionGenerator(get(partitionKeys), get(clusteringColumns), get(valueColumns), settings.generate.order); |
| } |
| |
| List<Generator> get(List<ColumnInfo> columnInfos) |
| { |
| List<Generator> result = new ArrayList<>(); |
| for (ColumnInfo columnInfo : columnInfos) |
| result.add(columnInfo.getGenerator()); |
| return result; |
| } |
| } |
| |
| static class ColumnInfo |
| { |
| final String name; |
| final DataType type; |
| final GeneratorConfig config; |
| |
| ColumnInfo(String name, DataType type, GeneratorConfig config) |
| { |
| this.name = name; |
| this.type = type; |
| this.config = config; |
| } |
| |
| Generator getGenerator() |
| { |
| return getGenerator(name, type, config); |
| } |
| |
| static Generator getGenerator(final String name, final DataType type, GeneratorConfig config) |
| { |
| switch (type.getName()) |
| { |
| case ASCII: |
| case TEXT: |
| case VARCHAR: |
| return new Strings(name, config); |
| case BIGINT: |
| case COUNTER: |
| return new Longs(name, config); |
| case BLOB: |
| return new Bytes(name, config); |
| case BOOLEAN: |
| return new Booleans(name, config); |
| case DECIMAL: |
| return new BigDecimals(name, config); |
| case DOUBLE: |
| return new Doubles(name, config); |
| case FLOAT: |
| return new Floats(name, config); |
| case INET: |
| return new Inets(name, config); |
| case INT: |
| return new Integers(name, config); |
| case VARINT: |
| return new BigIntegers(name, config); |
| case TIMESTAMP: |
| return new Dates(name, config); |
| case UUID: |
| return new UUIDs(name, config); |
| case TIMEUUID: |
| return new TimeUUIDs(name, config); |
| case TINYINT: |
| return new TinyInts(name, config); |
| case SMALLINT: |
| return new SmallInts(name, config); |
| case TIME: |
| return new Times(name, config); |
| case DATE: |
| return new LocalDates(name, config); |
| case SET: |
| return new Sets(name, getGenerator(name, type.getTypeArguments().get(0), config), config); |
| case LIST: |
| return new Lists(name, getGenerator(name, type.getTypeArguments().get(0), config), config); |
| default: |
| throw new UnsupportedOperationException("Because of this name: "+name+" if you removed it from the yaml and are still seeing this, make sure to drop table"); |
| } |
| } |
| } |
| |
| public static StressProfile load(URI file) throws IOError |
| { |
| try |
| { |
| Constructor constructor = new Constructor(StressYaml.class); |
| |
| Yaml yaml = new Yaml(constructor); |
| |
| InputStream yamlStream = file.toURL().openStream(); |
| |
| if (yamlStream.available() == 0) |
| throw new IOException("Unable to load yaml file from: "+file); |
| |
| StressYaml profileYaml = yaml.loadAs(yamlStream, StressYaml.class); |
| |
| StressProfile profile = new StressProfile(); |
| profile.init(profileYaml); |
| |
| return profile; |
| } |
| catch (YAMLException | IOException | RequestValidationException e) |
| { |
| throw new IOError(e); |
| } |
| } |
| |
| static <V> void lowerCase(Map<String, V> map) |
| { |
| List<Map.Entry<String, V>> reinsert = new ArrayList<>(); |
| Iterator<Map.Entry<String, V>> iter = map.entrySet().iterator(); |
| while (iter.hasNext()) |
| { |
| Map.Entry<String, V> e = iter.next(); |
| if (!e.getKey().equalsIgnoreCase(e.getKey())) |
| { |
| reinsert.add(e); |
| iter.remove(); |
| } |
| } |
| for (Map.Entry<String, V> e : reinsert) |
| map.put(e.getKey().toLowerCase(), e.getValue()); |
| } |
| } |