ImmutableSet<ListenableFuture<?>> index(List<Span> spans) { // First parse each span into partition keys used to support query requests Builder<PartitionKeyToTraceId, Long> parsed = ImmutableSetMultimap.builder(); for (Span span : spans) { Long timestamp = guessTimestamp(span); if (timestamp == null) continue; for (String partitionKey : index.partitionKeys(span)) { parsed.put( new PartitionKeyToTraceId(index.table(), partitionKey, span.traceId), 1000 * (timestamp / 1000)); // index precision is millis } } // The parsed results may include inserts that already occur, or are redundant as they don't // impact QueryRequest.endTs or QueryRequest.loopback. For example, a parsed timestamp could // be between timestamps of rows that already exist for a particular trace. ImmutableSetMultimap<PartitionKeyToTraceId, Long> maybeInsert = parsed.build(); ImmutableSetMultimap<PartitionKeyToTraceId, Long> toInsert; if (sharedState == null) { // special-case when caching is disabled. toInsert = maybeInsert; } else { // Optimized results will be smaller when the input includes traces with local spans, or when // other threads indexed the same trace. toInsert = entriesThatIncreaseGap(sharedState, maybeInsert); if (maybeInsert.size() > toInsert.size() && LOG.isDebugEnabled()) { int delta = maybeInsert.size() - toInsert.size(); LOG.debug("optimized out {}/{} inserts into {}", delta, maybeInsert.size(), index.table()); } } // For each entry, insert a new row in the index table asynchronously ImmutableSet.Builder<ListenableFuture<?>> result = ImmutableSet.builder(); for (Map.Entry<PartitionKeyToTraceId, Long> entry : toInsert.entries()) { BoundStatement bound = bindWithName(prepared, boundName) .setLong("trace_id", entry.getKey().traceId) .setBytesUnsafe("ts", timestampCodec.serialize(entry.getValue())); if (indexTtl != null) { bound.setInt("ttl_", indexTtl); } index.bindPartitionKey(bound, entry.getKey().partitionKey); result.add(session.executeAsync(bound)); } return result.build(); }
@VisibleForTesting static ImmutableSetMultimap<PartitionKeyToTraceId, Long> entriesThatIncreaseGap( ConcurrentMap<PartitionKeyToTraceId, Pair<Long>> sharedState, ImmutableSetMultimap<PartitionKeyToTraceId, Long> updates) { ImmutableSet.Builder<PartitionKeyToTraceId> toUpdate = ImmutableSet.builder(); // Enter a loop that affects shared state when an update widens the time interval for a key. for (Map.Entry<PartitionKeyToTraceId, Long> input : updates.entries()) { PartitionKeyToTraceId key = input.getKey(); long timestamp = input.getValue(); for (; ; ) { Pair<Long> oldRange = sharedState.get(key); if (oldRange == null) { // Initial state is where this key has a single timestamp. oldRange = sharedState.putIfAbsent(key, Pair.create(timestamp, timestamp)); // If there was no previous value, we need to update the index if (oldRange == null) { toUpdate.add(key); break; } } long first = timestamp < oldRange._1 ? timestamp : oldRange._1; long last = timestamp > oldRange._2 ? timestamp : oldRange._2; Pair<Long> newRange = Pair.create(first, last); if (oldRange.equals(newRange)) { break; // the current timestamp is contained } else if (sharedState.replace(key, oldRange, newRange)) { toUpdate.add(key); // The range was extended break; } } } // When the loop completes, we'll know one of our updates widened the interval of a trace, if // it is the first or last timestamp. By ignoring those between an existing interval, we can // end up with less Cassandra writes. Builder<PartitionKeyToTraceId, Long> result = ImmutableSetMultimap.builder(); for (PartitionKeyToTraceId needsUpdate : toUpdate.build()) { Pair<Long> firstLast = sharedState.get(needsUpdate); if (updates.containsEntry(needsUpdate, firstLast._1)) result.put(needsUpdate, firstLast._1); if (updates.containsEntry(needsUpdate, firstLast._2)) result.put(needsUpdate, firstLast._2); } return result.build(); }