/** * Copyright 2015-2016 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.cassandra; import com.datastax.driver.core.BoundStatement; import com.datastax.driver.core.Session; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Ticker; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.UncheckedExecutionException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import static zipkin.internal.Util.checkNotNull; /** * This reduces load on cassandra by preventing semantically equivalent requests from being invoked, * subject to a local TTL. * * <p>Ex. If you want to test that you don't repeatedly send bad data, you could send a 400 back. * * <pre>{@code * ttl = 60 * 1000; // 1 minute * deduper = new DeduplicatingExecutor(session, ttl); * * // the result of the first execution against "foo" is returned to other callers * // until it expires a minute later. * deduper.maybeExecute(bound, "foo"); * deduper.maybeExecute(bound, "foo"); * }</pre> */ class DeduplicatingExecutor { // not final for testing private final Session session; private final LoadingCache<BoundStatementKey, ListenableFuture<Void>> cache; /** * @param session which conditionally executes bound statements * @param ttl how long the results of statements are remembered, in milliseconds. */ DeduplicatingExecutor(Session session, long ttl) { this.session = session; this.cache = CacheBuilder.newBuilder() .expireAfterWrite(ttl, TimeUnit.MILLISECONDS) .ticker(new Ticker() { @Override public long read() { return nanoTime(); } }) // TODO: maximum size or weight .build(new CacheLoader<BoundStatementKey, ListenableFuture<Void>>() { @Override public ListenableFuture<Void> load(final BoundStatementKey key) { ListenableFuture<?> cassandraFuture = executeAsync(key.statement); // Drop the cassandra future so that we don't hold references to cassandra state for // long periods of time. final SettableFuture<Void> disconnectedFuture = SettableFuture.create(); Futures.addCallback(cassandraFuture, new FutureCallback<Object>() { @Override public void onSuccess(Object result) { disconnectedFuture.set(null); } @Override public void onFailure(Throwable t) { cache.invalidate(key); disconnectedFuture.setException(t); } }); return disconnectedFuture; } }); } /** * Upon success, the statement's result will be remembered and returned for all subsequent * executions with the same key, subject to a local TTL. * * <p>The results of failed statements are forgotten based on the supplied key. * * @param statement what to conditionally execute * @param key determines equivalence of the bound statement * @return future of work initiated by this or a previous request */ ListenableFuture<Void> maybeExecuteAsync(BoundStatement statement, Object key) { BoundStatementKey cacheKey = new BoundStatementKey(statement, key); try { ListenableFuture<Void> result = cache.get(new BoundStatementKey(statement, key)); // A future could be constructed directly (i.e. immediate future), get the value to // see if it was exceptional. If so, the catch block will invalidate that key. if (result.isDone()) result.get(); return result; } catch (UncheckedExecutionException | ExecutionException e) { cache.invalidate(cacheKey); return Futures.immediateFailedFuture(e.getCause()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new AssertionError(); } } // visible for testing, since nanoTime is weird and can return negative long nanoTime() { return System.nanoTime(); } @VisibleForTesting ListenableFuture<?> executeAsync(BoundStatement statement) { return session.executeAsync(statement); } @VisibleForTesting void clear() { cache.invalidateAll(); } /** Used to hold a reference to the last statement executed, but without using it in hashCode */ static final class BoundStatementKey { final BoundStatement statement; final Object key; BoundStatementKey(BoundStatement statement, Object key) { this.statement = checkNotNull(statement, "statement"); this.key = checkNotNull(key, "key"); } @Override public String toString() { return "(" + key + ", " + statement + ")"; } @Override public boolean equals(Object o) { if (o == this) return true; if (o instanceof BoundStatementKey) { return this.key.equals(((BoundStatementKey) o).key); } return false; } @Override public int hashCode() { return key.hashCode(); } } }