zipkin.storage.InMemorySpanStore Maven / Gradle / Ivy
/*
* Copyright 2015-2018 The OpenZipkin Authors
*
* 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 zipkin.storage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import zipkin.DependencyLink;
import zipkin.Span;
import zipkin.internal.CorrectForClockSkew;
import zipkin.internal.DependencyLinker;
import zipkin.internal.GroupByTraceId;
import zipkin.internal.MergeById;
import zipkin.internal.Nullable;
import zipkin.internal.Pair;
import static zipkin.internal.ApplyTimestampAndDuration.guessTimestamp;
import static zipkin.internal.GroupByTraceId.TRACE_DESCENDING;
import static zipkin.internal.Util.sortedList;
/**
* Internally, spans are indexed on 64-bit trace ID
*
* Here's an example of some traces in memory:
*
*
{@code
* spansByTraceIdTimeStamp:
* --> ( spanA(time:July 4, traceId:aaaa, service:foo, name:GET),
* spanB(time:July 4, traceId:aaaa, service:bar, name:GET) )
* --> ( spanC(time:July 4, traceId:aaaa, service:foo, name:GET) )
* --> ( spanD(time:July 5, traceId:bbbb, service:biz, name:GET) )
* --> ( spanE(time:July 6, traceId:bbbb) service:foo, name:POST )
*
* traceIdToTraceIdTimeStamps:
* aaaa --> [ ]
* bbbb --> [ , ]
* cccc --> [ ]
*
* serviceToTraceIds:
* foo --> [ , , ]
* bar --> [ ]
* biz --> [ ]
*
* serviceToSpanNames:
* bar --> ( GET )
* biz --> ( GET )
* foo --> ( GET, POST )
* }
*
* @deprecated use {@link zipkin2.storage.InMemoryStorage}
*/
@Deprecated
public final class InMemorySpanStore implements SpanStore {
/**
* Primary source of data is this map, which includes spans ordered descending by timestamp. All
* other maps are derived from the span values here. This uses a list for the spans, so that it is
* visible (via /api/v1/trace/id?raw) when instrumentation report the same spans multiple times.
*
* In the future, we will bound data. This implies some mechanism of cascading deletes based on
* the pair used as the key here. One implementation could be to make one dataset have a strong
* reference to the pair of (traceId, timestamp), and others weak. For example, making this a
* weak reference and traceIdToTraceIdTimeStamps a strong one. In that case, deleting the trace ID
* from traceIdToTraceIdTimeStamps would lead to a purge of spans at GC time.
*/
private final SortedMultimap, Span> spansByTraceIdTimeStamp =
new LinkedListSortedMultimap<>(VALUE_2_DESCENDING);
/** This supports span lookup by {@link zipkin.Span#traceId lower 64-bits of the trace ID} */
private final SortedMultimap> traceIdToTraceIdTimeStamps =
new LinkedHashSetSortedMultimap<>(LONG_COMPARATOR);
/** This is an index of {@link Span#traceId} by {@link zipkin.Endpoint#serviceName service name} */
private final ServiceNameToTraceIds serviceToTraceIds = new ServiceNameToTraceIds();
/** This is an index of {@link Span#name} by {@link zipkin.Endpoint#serviceName service name} */
private final SortedMultimap serviceToSpanNames =
new LinkedHashSetSortedMultimap<>(STRING_COMPARATOR);
private final boolean strictTraceId;
final int maxSpanCount;
volatile int acceptedSpanCount;
// Historical constructor
public InMemorySpanStore() {
this(new InMemoryStorage.Builder());
}
InMemorySpanStore(InMemoryStorage.Builder builder) {
this.strictTraceId = builder.strictTraceId;
this.maxSpanCount = builder.maxSpanCount;
}
final StorageAdapters.SpanConsumer spanConsumer = new StorageAdapters.SpanConsumer() {
@Override public void accept(List spans) {
if (spans.isEmpty()) return;
if (spans.size() > maxSpanCount) {
spans = spans.subList(0, maxSpanCount);
}
addSpans(spans);
}
@Override public String toString() {
return "InMemorySpanConsumer";
}
};
/**
* @deprecated use {@link #getRawTraces()}
*/
@Deprecated
public synchronized List traceIds() {
return sortedList(traceIdToTraceIdTimeStamps.keySet());
}
synchronized void clear() {
acceptedSpanCount = 0;
traceIdToTraceIdTimeStamps.clear();
spansByTraceIdTimeStamp.clear();
serviceToTraceIds.clear();
serviceToSpanNames.clear();
}
synchronized void addSpans(List spans) {
int delta = spans.size();
int spansToRecover = (spansByTraceIdTimeStamp.size() + delta) - maxSpanCount;
evictToRecoverSpans(spansToRecover);
for (Span span : spans) {
Long timestamp = guessTimestamp(span);
Pair traceIdTimeStamp =
Pair.create(span.traceId, timestamp == null ? Long.MIN_VALUE : timestamp);
String spanName = span.name;
spansByTraceIdTimeStamp.put(traceIdTimeStamp, span);
traceIdToTraceIdTimeStamps.put(span.traceId, traceIdTimeStamp);
acceptedSpanCount++;
for (String serviceName : span.serviceNames()) {
serviceToTraceIds.put(serviceName, span.traceId);
serviceToSpanNames.put(serviceName, spanName);
}
}
}
/** Returns the count of spans evicted. */
int evictToRecoverSpans(int spansToRecover) {
int spansEvicted = 0;
while (spansToRecover > 0) {
int spansInOldestTrace = deleteOldestTrace();
spansToRecover -= spansInOldestTrace;
spansEvicted += spansInOldestTrace;
}
return spansEvicted;
}
/** Returns the count of spans evicted. */
private int deleteOldestTrace() {
int spansEvicted = 0;
long traceId = spansByTraceIdTimeStamp.delegate.lastKey()._1;
Collection> traceIdTimeStamps = traceIdToTraceIdTimeStamps.remove(traceId);
for (Iterator> traceIdTimeStampIter = traceIdTimeStamps.iterator();
traceIdTimeStampIter.hasNext(); ) {
Pair traceIdTimeStamp = traceIdTimeStampIter.next();
Collection spans = spansByTraceIdTimeStamp.remove(traceIdTimeStamp);
spansEvicted += spans.size();
}
for (String orphanedService : serviceToTraceIds.removeServiceIfTraceId(traceId)) {
serviceToSpanNames.remove(orphanedService);
}
return spansEvicted;
}
/**
* Used for testing. Returns all traces unconditionally.
*/
public synchronized List> getRawTraces() {
List> result = new ArrayList<>();
for (long traceId : traceIdToTraceIdTimeStamps.keySet()) {
Collection sameTraceId = spansByTraceId(traceId);
for (List next : GroupByTraceId.apply(sameTraceId, strictTraceId, false)) {
result.add(next);
}
}
Collections.sort(result, TRACE_DESCENDING);
return result;
}
@Override public List> getTraces(QueryRequest request) {
return getTraces(request, strictTraceId);
}
synchronized List> getTraces(QueryRequest request, boolean strictTraceId) {
Set traceIdsInTimerange = traceIdsDescendingByTimestamp(request);
if (traceIdsInTimerange.isEmpty()) return Collections.emptyList();
List> result = new ArrayList<>();
for (Iterator traceId = traceIdsInTimerange.iterator();
traceId.hasNext() && result.size() < request.limit; ) {
Collection sameTraceId = spansByTraceId(traceId.next());
for (List next : GroupByTraceId.apply(sameTraceId, strictTraceId, true)) {
if (request.test(next)) {
result.add(next);
}
}
}
Collections.sort(result, TRACE_DESCENDING);
return result;
}
Set traceIdsDescendingByTimestamp(QueryRequest request) {
Collection> traceIdTimestamps = request.serviceName != null
? traceIdTimestampsByServiceName(request.serviceName)
: spansByTraceIdTimeStamp.keySet();
long endTs = request.endTs * 1000;
long startTs = endTs - request.lookback * 1000;
if (traceIdTimestamps == null || traceIdTimestamps.isEmpty()) return Collections.emptySet();
Set result = new LinkedHashSet<>();
for (Pair traceIdTimestamp : traceIdTimestamps) {
if (traceIdTimestamp._2 >= startTs || traceIdTimestamp._2 <= endTs) {
result.add(traceIdTimestamp._1);
}
}
return result;
}
@Override public synchronized List getTrace(long traceId) {
return getTrace(0L, traceId);
}
@Override public synchronized List getTrace(long traceIdHigh, long traceIdLow) {
List result = getRawTrace(traceIdHigh, traceIdLow);
if (result == null) return null;
return CorrectForClockSkew.apply(MergeById.apply(result));
}
@Override public synchronized List getRawTrace(long traceId) {
return getRawTrace(0L, traceId);
}
@Override public synchronized List getRawTrace(long traceIdHigh, long traceId) {
List spans = (List) spansByTraceId(traceId);
if (spans == null || spans.isEmpty()) return null;
if (!strictTraceId) return sortedList(spans);
List filtered = new ArrayList<>(spans);
Iterator iterator = filtered.iterator();
while (iterator.hasNext()) {
if (iterator.next().traceIdHigh != traceIdHigh) {
iterator.remove();
}
}
return filtered.isEmpty() ? null : filtered;
}
@Override
public synchronized List getServiceNames() {
return sortedList(serviceToTraceIds.keySet());
}
@Override
public synchronized List getSpanNames(String service) {
if (service == null) return Collections.emptyList();
service = service.toLowerCase(); // service names are always lowercase!
return sortedList(serviceToSpanNames.get(service));
}
@Override
public synchronized List getDependencies(long endTs, @Nullable Long lookback) {
QueryRequest request = QueryRequest.builder()
.endTs(endTs)
.lookback(lookback)
.limit(Integer.MAX_VALUE).build();
DependencyLinker linksBuilder = new DependencyLinker();
// We don't have a query parameter for strictTraceId when fetching dependency links, so we
// ignore traceIdHigh. Otherwise, a single trace can appear as two, doubling callCount.
for (Collection trace : getTraces(request, /* strictTraceId */false)) {
linksBuilder.putTrace(trace);
}
return linksBuilder.link();
}
static final class LinkedListSortedMultimap extends SortedMultimap {
LinkedListSortedMultimap(Comparator comparator) {
super(comparator);
}
@Override Collection valueContainer() {
return new ArrayList<>();
}
}
static final Comparator STRING_COMPARATOR = new Comparator() {
@Override public int compare(String left, String right) {
if (left == null) return -1;
return left.compareTo(right);
}
@Override public String toString() {
return "String::compareTo";
}
};
static final Comparator LONG_COMPARATOR = new Comparator() {
@Override public int compare(Long x, Long y) {
if (x == null) return -1;
return (x < y) ? -1 : ((x.equals(y)) ? 0 : 1);
}
@Override public String toString() {
return "Long::compareTo"; // Long.compareTo is JRE 7+
}
};
static final Comparator> VALUE_2_DESCENDING = new Comparator>() {
@Override public int compare(Pair left, Pair right) {
long x = left._2, y = right._2;
int result = (x < y) ? -1 : ((x == y) ? 0 : 1); // Long.compareTo is JRE 7+
if (result != 0) return -result; // use negative as we are descending
// secondary compare as TreeMap is in use
x = left._1;
y = right._1;
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
@Override public String toString() {
return "Value2Descending{}";
}
};
static final class ServiceNameToTraceIds extends SortedMultimap {
ServiceNameToTraceIds() {
super(STRING_COMPARATOR);
}
@Override Set valueContainer() {
return new LinkedHashSet<>();
}
/** Returns service names orphaned by removing the trace ID */
Set removeServiceIfTraceId(long traceId) {
Set result = new LinkedHashSet<>();
for (Map.Entry> entry : delegate.entrySet()) {
Collection traceIds = entry.getValue();
if (traceIds.remove(traceId) && traceIds.isEmpty()) {
result.add(entry.getKey());
}
}
delegate.keySet().removeAll(result);
return result;
}
}
static final class LinkedHashSetSortedMultimap extends SortedMultimap {
LinkedHashSetSortedMultimap(Comparator comparator) {
super(comparator);
}
@Override Collection valueContainer() {
return new LinkedHashSet<>();
}
}
// Not synchronized as every exposed method on the enclosing type is
static abstract class SortedMultimap {
final TreeMap> delegate;
int size = 0;
SortedMultimap(Comparator comparator) {
delegate = new TreeMap<>(comparator);
}
abstract Collection valueContainer();
Set keySet() {
return delegate.keySet();
}
int size() {
return size;
}
void put(K key, V value) {
Collection valueContainer = delegate.get(key);
if (valueContainer == null) {
delegate.put(key, valueContainer = valueContainer());
}
if (valueContainer.add(value)) size++;
}
Collection remove(K key) {
Collection value = delegate.remove(key);
if (value != null) size -= value.size();
return value;
}
void clear() {
delegate.clear();
size = 0;
}
Collection get(K key) {
Collection result = delegate.get(key);
return result != null ? result : Collections.emptySet();
}
}
private Collection spansByTraceId(long traceId) {
Collection sameTraceId = new ArrayList<>();
for (Pair traceIdTimestamp : traceIdToTraceIdTimeStamps.get(traceId)) {
sameTraceId.addAll(spansByTraceIdTimeStamp.get(traceIdTimestamp));
}
return sameTraceId;
}
private Collection> traceIdTimestampsByServiceName(String serviceName) {
List> traceIdTimestamps = new ArrayList<>();
for (long traceId : serviceToTraceIds.get(serviceName)) {
traceIdTimestamps.addAll(traceIdToTraceIdTimeStamps.get(traceId));
}
Collections.sort(traceIdTimestamps, VALUE_2_DESCENDING);
return traceIdTimestamps;
}
}