diff --git a/atlasdb-api/src/main/java/com/palantir/atlasdb/spi/AtlasDbFactory.java b/atlasdb-api/src/main/java/com/palantir/atlasdb/spi/AtlasDbFactory.java index c85ca3c1466..28942d771cb 100644 --- a/atlasdb-api/src/main/java/com/palantir/atlasdb/spi/AtlasDbFactory.java +++ b/atlasdb-api/src/main/java/com/palantir/atlasdb/spi/AtlasDbFactory.java @@ -37,7 +37,7 @@ public interface AtlasDbFactory { default KeyValueService createRawKeyValueService( KeyValueServiceConfig config, Optional leaderConfig) { return createRawKeyValueService(config, leaderConfig, Optional.empty(), DEFAULT_INITIALIZE_ASYNC, - FakeQosClient.getDefault()); + FakeQosClient.INSTANCE); } /** diff --git a/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/CassandraClientFactory.java b/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/CassandraClientFactory.java index 3d8a3a5e7d8..a29facdd13a 100644 --- a/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/CassandraClientFactory.java +++ b/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/CassandraClientFactory.java @@ -46,6 +46,7 @@ import com.google.common.collect.Maps; import com.palantir.atlasdb.cassandra.CassandraCredentialsConfig; import com.palantir.atlasdb.cassandra.CassandraKeyValueServiceConfig; +import com.palantir.atlasdb.keyvalue.cassandra.qos.QosCassandraClient; import com.palantir.atlasdb.qos.QosClient; import com.palantir.atlasdb.util.AtlasDbMetrics; import com.palantir.common.exception.AtlasDbDependencyException; diff --git a/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/CassandraClientPoolImpl.java b/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/CassandraClientPoolImpl.java index 5125de764c1..c4f020280ff 100644 --- a/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/CassandraClientPoolImpl.java +++ b/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/CassandraClientPoolImpl.java @@ -164,11 +164,11 @@ public void shutdown() { @VisibleForTesting static CassandraClientPoolImpl createImplForTest(CassandraKeyValueServiceConfig config, StartupChecks startupChecks) { - return create(config, startupChecks, AtlasDbConstants.DEFAULT_INITIALIZE_ASYNC, FakeQosClient.getDefault()); + return create(config, startupChecks, AtlasDbConstants.DEFAULT_INITIALIZE_ASYNC, FakeQosClient.INSTANCE); } public static CassandraClientPool create(CassandraKeyValueServiceConfig config) { - return create(config, AtlasDbConstants.DEFAULT_INITIALIZE_ASYNC, FakeQosClient.getDefault()); + return create(config, AtlasDbConstants.DEFAULT_INITIALIZE_ASYNC, FakeQosClient.INSTANCE); } public static CassandraClientPool create(CassandraKeyValueServiceConfig config, boolean initializeAsync, diff --git a/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/CassandraExpiringKeyValueService.java b/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/CassandraExpiringKeyValueService.java index dd66214bce2..54ab0a5b292 100644 --- a/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/CassandraExpiringKeyValueService.java +++ b/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/CassandraExpiringKeyValueService.java @@ -69,7 +69,7 @@ private CassandraExpiringKeyValueService( Optional leaderConfig, boolean initializeAsync) { super(LoggerFactory.getLogger(CassandraKeyValueService.class), configManager, compactionManager, leaderConfig, - initializeAsync, FakeQosClient.getDefault()); + initializeAsync, FakeQosClient.INSTANCE); } @Override diff --git a/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/CassandraKeyValueServiceImpl.java b/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/CassandraKeyValueServiceImpl.java index c4bece52c64..f87a0478c66 100644 --- a/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/CassandraKeyValueServiceImpl.java +++ b/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/CassandraKeyValueServiceImpl.java @@ -122,6 +122,8 @@ import com.palantir.atlasdb.logging.LoggingArgs; import com.palantir.atlasdb.qos.FakeQosClient; import com.palantir.atlasdb.qos.QosClient; +import com.palantir.atlasdb.qos.ratelimit.QosAwareThrowables; +import com.palantir.atlasdb.qos.ratelimit.RateLimitExceededException; import com.palantir.atlasdb.util.AnnotatedCallable; import com.palantir.atlasdb.util.AnnotationType; import com.palantir.atlasdb.util.AtlasDbMetrics; @@ -129,7 +131,6 @@ import com.palantir.common.base.ClosableIterator; import com.palantir.common.base.ClosableIterators; import com.palantir.common.base.FunctionCheckedException; -import com.palantir.common.base.Throwables; import com.palantir.common.exception.PalantirRuntimeException; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.UnsafeArg; @@ -224,7 +225,7 @@ public static CassandraKeyValueService create( CassandraKeyValueServiceConfigManager configManager, Optional leaderConfig, boolean initializeAsync) { - return create(configManager, leaderConfig, initializeAsync, FakeQosClient.getDefault()); + return create(configManager, leaderConfig, initializeAsync, FakeQosClient.INSTANCE); } public static CassandraKeyValueService create( @@ -245,7 +246,7 @@ static CassandraKeyValueService create( Optional leaderConfig, Logger log) { return create(configManager, leaderConfig, log, AtlasDbConstants.DEFAULT_INITIALIZE_ASYNC, - FakeQosClient.getDefault()); + FakeQosClient.INSTANCE); } private static CassandraKeyValueService create( @@ -497,7 +498,7 @@ public String toString() { } return ImmutableMap.copyOf(result); } catch (Exception e) { - throw Throwables.unwrapAndThrowAtlasDbDependencyException(e); + throw QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException(e); } } @@ -567,7 +568,7 @@ public Map get(TableReference tableRef, Map timestampBy } return builder.build(); } catch (Exception e) { - throw Throwables.unwrapAndThrowAtlasDbDependencyException(e); + throw QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException(e); } } @@ -795,7 +796,7 @@ private Map getRowsColumnRangeIteratorForSingleH } return ret; } catch (Exception e) { - throw Throwables.unwrapAndThrowAtlasDbDependencyException(e); + throw QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException(e); } } @@ -835,7 +836,7 @@ public String toString() { } }); } catch (Exception e) { - throw Throwables.unwrapAndThrowAtlasDbDependencyException(e); + throw QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException(e); } } @@ -975,7 +976,7 @@ public void put(final TableReference tableRef, final Map values, f try { putInternal("put", tableRef, KeyValueServices.toConstantTimestampValues(values.entrySet(), timestamp)); } catch (Exception e) { - throw Throwables.unwrapAndThrowAtlasDbDependencyException(e); + throw QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException(e); } } @@ -995,7 +996,7 @@ public void putWithTimestamps(TableReference tableRef, Multimap val try { putInternal("putWithTimestamps", tableRef, values.entries()); } catch (Exception e) { - throw Throwables.unwrapAndThrowAtlasDbDependencyException(e); + throw QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException(e); } } @@ -1284,7 +1285,7 @@ public void truncateTables(final Set tablesToTruncate) { throw new InsufficientConsistencyException("Truncating tables requires all Cassandra nodes" + " to be up and available."); } catch (TException e) { - throw Throwables.unwrapAndThrowAtlasDbDependencyException(e); + throw QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException(e); } } } @@ -1416,7 +1417,7 @@ public String toString() { throw new InsufficientConsistencyException("Deleting requires all Cassandra nodes to be up and available.", e); } catch (Exception e) { - throw Throwables.unwrapAndThrowAtlasDbDependencyException(e); + throw QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException(e); } } @@ -1635,7 +1636,7 @@ private void dropTablesInternal(final Set tablesToDrop) { throw new InsufficientConsistencyException( "Dropping tables requires all Cassandra nodes to be up and available.", e); } catch (Exception e) { - throw Throwables.unwrapAndThrowAtlasDbDependencyException(e); + throw QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException(e); } } @@ -1761,7 +1762,7 @@ private Map filterOutExistingTables( } } } catch (Exception e) { - throw Throwables.unwrapAndThrowAtlasDbDependencyException(e); + throw QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException(e); } return filteredTables; @@ -1782,7 +1783,8 @@ private void createTablesInternal(final Map tableNamesTo } catch (TException thriftException) { if (thriftException.getMessage() != null && !thriftException.getMessage().contains("already existing table")) { - throw Throwables.unwrapAndThrowAtlasDbDependencyException(thriftException); + throw QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException( + thriftException); } } } @@ -2024,7 +2026,7 @@ private void putMetadataAndMaybeAlterTables( return null; }); } catch (Exception e) { - throw Throwables.unwrapAndThrowAtlasDbDependencyException(e); + throw QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException(e); } } @@ -2089,7 +2091,7 @@ public void addGarbageCollectionSentinelValues(TableReference tableRef, Iterable putInternal("addGarbageCollectionSentinelValues", tableRef, Iterables.transform(cells, cell -> Maps.immutableEntry(cell, value))); } catch (Exception e) { - throw Throwables.unwrapAndThrowAtlasDbDependencyException(e); + throw QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException(e); } } @@ -2146,7 +2148,7 @@ public void putUnlessExists(final TableReference tableRef, final Map List runAllTasksCancelOnFailure(List> tasks) { try { //Callable returns null, so can't use immutable list return Collections.singletonList(tasks.get(0).call()); + } catch (RateLimitExceededException e) { + // Prioritise over + throw e; } catch (Exception e) { - throw Throwables.unwrapAndThrowAtlasDbDependencyException(e); + throw QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException(e); } } @@ -2486,7 +2491,7 @@ private List runAllTasksCancelOnFailure(List> tasks) { } return results; } catch (Exception e) { - throw Throwables.unwrapAndThrowAtlasDbDependencyException(e); + throw QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException(e); } finally { for (Future future : futures) { future.cancel(true); diff --git a/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/QosMetrics.java b/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/QosMetrics.java deleted file mode 100644 index 23b3d9e58a8..00000000000 --- a/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/QosMetrics.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2017 Palantir Technologies, Inc. All rights reserved. - * - * Licensed under the BSD-3 License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://opensource.org/licenses/BSD-3-Clause - * - * 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 com.palantir.atlasdb.keyvalue.cassandra; - -import com.codahale.metrics.Meter; -import com.palantir.atlasdb.util.MetricsManager; - -public class QosMetrics { - private final MetricsManager metricsManager = new MetricsManager(); - - private final Meter readRequestCount; - private final Meter writeRequestCount; - private final Meter bytesRead; - private final Meter bytesWritten; - - public QosMetrics() { - readRequestCount = metricsManager.registerMeter(QosMetrics.class, "numReadRequests"); - writeRequestCount = metricsManager.registerMeter(QosMetrics.class, "numWriteRequests"); - - bytesRead = metricsManager.registerMeter(QosMetrics.class, "bytesRead"); - bytesWritten = metricsManager.registerMeter(QosMetrics.class, "bytesWritten"); - } - - public void updateReadCount() { - readRequestCount.mark(); - } - - public void updateWriteCount() { - writeRequestCount.mark(); - } - - public void updateBytesRead(long numBytes) { - bytesRead.mark(numBytes); - } - - public void updateBytesWritten(long numBytes) { - bytesWritten.mark(numBytes); - } -} diff --git a/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/paging/RowGetter.java b/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/paging/RowGetter.java index f27f853f1f0..8d8227aaf5f 100644 --- a/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/paging/RowGetter.java +++ b/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/paging/RowGetter.java @@ -29,8 +29,8 @@ import com.palantir.atlasdb.keyvalue.cassandra.CassandraClient; import com.palantir.atlasdb.keyvalue.cassandra.CassandraClientPool; import com.palantir.atlasdb.keyvalue.cassandra.TracingQueryRunner; +import com.palantir.atlasdb.qos.ratelimit.QosAwareThrowables; import com.palantir.common.base.FunctionCheckedException; -import com.palantir.common.base.Throwables; public class RowGetter { private CassandraClientPool clientPool; @@ -69,7 +69,7 @@ public List apply(CassandraClient client) throws Exception { throw new InsufficientConsistencyException("get_range_slices requires " + consistency + " Cassandra nodes to be up and available.", e); } catch (Exception e) { - throw Throwables.unwrapAndThrowAtlasDbDependencyException(e); + throw QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException(e); } } diff --git a/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/QosCassandraClient.java b/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/qos/QosCassandraClient.java similarity index 50% rename from atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/QosCassandraClient.java rename to atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/qos/QosCassandraClient.java index bfaec55afda..3a8a658965f 100644 --- a/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/QosCassandraClient.java +++ b/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/qos/QosCassandraClient.java @@ -14,14 +14,11 @@ * limitations under the License. */ -package com.palantir.atlasdb.keyvalue.cassandra; +package com.palantir.atlasdb.keyvalue.cassandra.qos; import java.nio.ByteBuffer; -import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.function.Function; -import java.util.function.Supplier; import org.apache.cassandra.thrift.CASResult; import org.apache.cassandra.thrift.Cassandra; @@ -44,20 +41,21 @@ import org.slf4j.LoggerFactory; import com.palantir.atlasdb.keyvalue.api.TableReference; +import com.palantir.atlasdb.keyvalue.cassandra.CassandraClient; +import com.palantir.atlasdb.keyvalue.cassandra.CqlQuery; import com.palantir.atlasdb.qos.QosClient; @SuppressWarnings({"all"}) // thrift variable names. public class QosCassandraClient implements CassandraClient { - private final Logger log = LoggerFactory.getLogger(CassandraClient.class); + + private static final Logger log = LoggerFactory.getLogger(CassandraClient.class); private final CassandraClient client; - private final QosMetrics qosMetrics; private final QosClient qosClient; public QosCassandraClient(CassandraClient client, QosClient qosClient) { this.client = client; this.qosClient = qosClient; - qosMetrics = new QosMetrics(); } @Override @@ -69,105 +67,57 @@ public Cassandra.Client rawClient() { public Map> multiget_slice(String kvsMethodName, TableReference tableRef, List keys, SlicePredicate predicate, ConsistencyLevel consistency_level) throws InvalidRequestException, UnavailableException, TimedOutException, TException { - qosClient.checkLimit(); - - Map> result = client.multiget_slice(kvsMethodName, tableRef, keys, - predicate, consistency_level); - recordBytesRead(() -> getApproximateReadByteCount(result)); - return result; - } - - private long getApproximateReadByteCount(Map> result) { - return getCollectionSize(result.entrySet(), - rowResult -> ThriftObjectSizeUtils.getByteBufferSize(rowResult.getKey()) - + getCollectionSize(rowResult.getValue(), - ThriftObjectSizeUtils::getColumnOrSuperColumnSize)); + return qosClient.executeRead( + () -> client.multiget_slice(kvsMethodName, tableRef, keys, predicate, consistency_level), + ThriftQueryWeighers.MULTIGET_SLICE); } @Override public List get_range_slices(String kvsMethodName, TableReference tableRef, SlicePredicate predicate, KeyRange range, ConsistencyLevel consistency_level) throws InvalidRequestException, UnavailableException, TimedOutException, TException { - qosClient.checkLimit(); - - List result = client.get_range_slices(kvsMethodName, tableRef, predicate, range, consistency_level); - recordBytesRead(() -> getCollectionSize(result, ThriftObjectSizeUtils::getKeySliceSize)); - return result; + return qosClient.executeRead( + () -> client.get_range_slices(kvsMethodName, tableRef, predicate, range, consistency_level), + ThriftQueryWeighers.GET_RANGE_SLICES); } @Override public void batch_mutate(String kvsMethodName, Map>> mutation_map, ConsistencyLevel consistency_level) throws InvalidRequestException, UnavailableException, TimedOutException, TException { - qosClient.checkLimit(); - - client.batch_mutate(kvsMethodName, mutation_map, consistency_level); - recordBytesWritten(() -> getApproximateWriteByteCount(mutation_map)); - } - - private long getApproximateWriteByteCount(Map>> batchMutateMap) { - long approxBytesForKeys = getCollectionSize(batchMutateMap.keySet(), ThriftObjectSizeUtils::getByteBufferSize); - long approxBytesForValues = getCollectionSize(batchMutateMap.values(), currentMap -> - getCollectionSize(currentMap.keySet(), ThriftObjectSizeUtils::getStringSize) - + getCollectionSize(currentMap.values(), - mutations -> getCollectionSize(mutations, ThriftObjectSizeUtils::getMutationSize))); - return approxBytesForKeys + approxBytesForValues; + qosClient.executeWrite( + () -> { + client.batch_mutate(kvsMethodName, mutation_map, consistency_level); + return null; + }, + ThriftQueryWeighers.batchMutate(mutation_map)); } @Override public ColumnOrSuperColumn get(TableReference tableReference, ByteBuffer key, byte[] column, ConsistencyLevel consistency_level) throws InvalidRequestException, NotFoundException, UnavailableException, TimedOutException, TException { - qosClient.checkLimit(); - - ColumnOrSuperColumn result = client.get(tableReference, key, column, consistency_level); - recordBytesRead(() -> ThriftObjectSizeUtils.getColumnOrSuperColumnSize(result)); - return result; + return qosClient.executeRead( + () -> client.get(tableReference, key, column, consistency_level), + ThriftQueryWeighers.GET); } @Override public CASResult cas(TableReference tableReference, ByteBuffer key, List expected, List updates, ConsistencyLevel serial_consistency_level, ConsistencyLevel commit_consistency_level) throws InvalidRequestException, UnavailableException, TimedOutException, TException { - qosClient.checkLimit(); - - CASResult result = client.cas(tableReference, key, expected, updates, serial_consistency_level, - commit_consistency_level); - recordBytesWritten(() -> getCollectionSize(updates, ThriftObjectSizeUtils::getColumnSize)); - recordBytesRead(() -> getCollectionSize(updates, ThriftObjectSizeUtils::getColumnSize)); - return result; + // CAS is intentionally not rate limited, until we have a concept of priority + return client.cas(tableReference, key, expected, updates, serial_consistency_level, commit_consistency_level); } @Override public CqlResult execute_cql3_query(CqlQuery cqlQuery, Compression compression, ConsistencyLevel consistency) throws InvalidRequestException, UnavailableException, TimedOutException, SchemaDisagreementException, TException { - qosClient.checkLimit(); - - CqlResult cqlResult = client.execute_cql3_query(cqlQuery, compression, consistency); - recordBytesRead(() -> ThriftObjectSizeUtils.getCqlResultSize(cqlResult)); - return cqlResult; + return qosClient.executeRead( + () -> client.execute_cql3_query(cqlQuery, compression, consistency), + ThriftQueryWeighers.EXECUTE_CQL3_QUERY); } - private void recordBytesRead(Supplier numBytesRead) { - try { - qosMetrics.updateReadCount(); - qosMetrics.updateBytesRead(numBytesRead.get()); - } catch (Exception e) { - log.warn("Encountered an exception when recording read metrics.", e); - } - } - private void recordBytesWritten(Supplier numBytesWritten) { - try { - qosMetrics.updateWriteCount(); - qosMetrics.updateBytesWritten(numBytesWritten.get()); - } catch (Exception e) { - log.warn("Encountered an exception when recording write metrics.", e); - } - } - - private long getCollectionSize(Collection collection, Function singleObjectSizeFunction) { - return ThriftObjectSizeUtils.getCollectionSize(collection, singleObjectSizeFunction); - } } diff --git a/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/ThriftObjectSizeUtils.java b/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/qos/ThriftObjectSizeUtils.java similarity index 81% rename from atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/ThriftObjectSizeUtils.java rename to atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/qos/ThriftObjectSizeUtils.java index b973bfc707f..85146ed117e 100644 --- a/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/ThriftObjectSizeUtils.java +++ b/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/qos/ThriftObjectSizeUtils.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package com.palantir.atlasdb.keyvalue.cassandra; +package com.palantir.atlasdb.keyvalue.cassandra.qos; import java.nio.ByteBuffer; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.function.Function; @@ -37,12 +38,38 @@ public final class ThriftObjectSizeUtils { - private static final long ONE_BYTE = 1L; + private static final long ONE_BYTE = 1; private ThriftObjectSizeUtils() { // utility class } + public static long getApproximateWriteByteCount(Map>> batchMutateMap) { + long approxBytesForKeys = getCollectionSize(batchMutateMap.keySet(), ThriftObjectSizeUtils::getByteBufferSize); + long approxBytesForValues = getCollectionSize(batchMutateMap.values(), + currentMap -> getCollectionSize(currentMap.keySet(), ThriftObjectSizeUtils::getStringSize) + + getCollectionSize(currentMap.values(), + mutations -> getCollectionSize(mutations, ThriftObjectSizeUtils::getMutationSize))); + return approxBytesForKeys + approxBytesForValues; + } + + public static long getApproximateReadByteCount(Map> result) { + return getCollectionSize(result.entrySet(), + rowResult -> ThriftObjectSizeUtils.getByteBufferSize(rowResult.getKey()) + + getCollectionSize(rowResult.getValue(), + ThriftObjectSizeUtils::getColumnOrSuperColumnSize)); + } + + public static long getApproximateReadByteCount(List slices) { + return getCollectionSize(slices, ThriftObjectSizeUtils::getKeySliceSize); + } + + public static long getCasByteCount(List updates) { + // TODO(nziebart): CAS actually writes more bytes than this, because the associated Paxos negotations must + // be persisted + return getCollectionSize(updates, ThriftObjectSizeUtils::getColumnSize); + } + public static long getColumnOrSuperColumnSize(ColumnOrSuperColumn columnOrSuperColumn) { if (columnOrSuperColumn == null) { return getNullSize(); @@ -93,7 +120,7 @@ public static long getStringSize(String string) { return getNullSize(); } - return string.length() * Character.SIZE; + return string.length(); } public static long getColumnSize(Column column) { @@ -180,7 +207,7 @@ private static long getByteBufferStringMapSize(Map nameTypes + ThriftObjectSizeUtils.getStringSize(entry.getValue())); } - private static Long getCqlRowSize(CqlRow cqlRow) { + private static long getCqlRowSize(CqlRow cqlRow) { if (cqlRow == null) { return getNullSize(); } diff --git a/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/qos/ThriftQueryWeighers.java b/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/qos/ThriftQueryWeighers.java new file mode 100644 index 00000000000..5d31a1267a0 --- /dev/null +++ b/atlasdb-cassandra/src/main/java/com/palantir/atlasdb/keyvalue/cassandra/qos/ThriftQueryWeighers.java @@ -0,0 +1,141 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the BSD-3 License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/BSD-3-Clause + * + * 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 com.palantir.atlasdb.keyvalue.cassandra.qos; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.apache.cassandra.thrift.ColumnOrSuperColumn; +import org.apache.cassandra.thrift.CqlResult; +import org.apache.cassandra.thrift.KeySlice; +import org.apache.cassandra.thrift.Mutation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Suppliers; +import com.palantir.atlasdb.keyvalue.cassandra.CassandraClient; +import com.palantir.atlasdb.qos.ImmutableQueryWeight; +import com.palantir.atlasdb.qos.QosClient; +import com.palantir.atlasdb.qos.QueryWeight; + +public final class ThriftQueryWeighers { + + private static final Logger log = LoggerFactory.getLogger(CassandraClient.class); + + @VisibleForTesting + static final QueryWeight DEFAULT_ESTIMATED_WEIGHT = ImmutableQueryWeight.builder() + .numBytes(100) + .numDistinctRows(1) + .timeTakenNanos(TimeUnit.MILLISECONDS.toNanos(2)) + .build(); + + private ThriftQueryWeighers() { } + + public static final QosClient.QueryWeigher>> MULTIGET_SLICE = + readWeigher(ThriftObjectSizeUtils::getApproximateReadByteCount, Map::size); + + public static final QosClient.QueryWeigher> GET_RANGE_SLICES = + readWeigher(ThriftObjectSizeUtils::getApproximateReadByteCount, List::size); + + public static final QosClient.QueryWeigher GET = + readWeigher(ThriftObjectSizeUtils::getColumnOrSuperColumnSize, ignored -> 1); + + public static final QosClient.QueryWeigher EXECUTE_CQL3_QUERY = + // TODO(nziebart): we need to inspect the schema to see how many rows there are - a CQL row is NOT a + // partition. rows here will depend on the type of query executed in CqlExecutor: either (column, ts) pairs, + // or (key, column, ts) triplets + readWeigher(ThriftObjectSizeUtils::getCqlResultSize, ignored -> 1); + + public static QosClient.QueryWeigher batchMutate( + Map>> mutationMap) { + long numRows = mutationMap.size(); + return writeWeigher(numRows, () -> ThriftObjectSizeUtils.getApproximateWriteByteCount(mutationMap)); + } + + public static QosClient.QueryWeigher readWeigher(Function bytesRead, Function numRows) { + return new QosClient.QueryWeigher() { + @Override + public QueryWeight estimate() { + return DEFAULT_ESTIMATED_WEIGHT; + } + + @Override + public QueryWeight weighSuccess(T result, long timeTakenNanos) { + return ImmutableQueryWeight.builder() + .numBytes(safeGetNumBytesOrDefault(() -> bytesRead.apply(result))) + .timeTakenNanos(timeTakenNanos) + .numDistinctRows(numRows.apply(result)) + .build(); + } + + @Override + public QueryWeight weighFailure(Exception error, long timeTakenNanos) { + return ImmutableQueryWeight.builder() + .from(estimate()) + .timeTakenNanos(timeTakenNanos) + .build(); + } + }; + } + + public static QosClient.QueryWeigher writeWeigher(long numRows, Supplier bytesWritten) { + Supplier weight = Suppliers.memoize(() -> safeGetNumBytesOrDefault(bytesWritten))::get; + + return new QosClient.QueryWeigher() { + @Override + public QueryWeight estimate() { + return ImmutableQueryWeight.builder() + .from(DEFAULT_ESTIMATED_WEIGHT) + .numBytes(weight.get()) + .numDistinctRows(numRows) + .build(); + } + + @Override + public QueryWeight weighSuccess(T result, long timeTakenNanos) { + return ImmutableQueryWeight.builder() + .from(estimate()) + .timeTakenNanos(timeTakenNanos) + .build(); + } + + @Override + public QueryWeight weighFailure(Exception error, long timeTakenNanos) { + return ImmutableQueryWeight.builder() + .from(estimate()) + .timeTakenNanos(timeTakenNanos) + .build(); + } + }; + } + + // TODO(nziebart): we really shouldn't be needing to catch exceptions here + private static long safeGetNumBytesOrDefault(Supplier numBytes) { + try { + return numBytes.get(); + } catch (Exception e) { + log.warn("Error calculating number of bytes", e); + return DEFAULT_ESTIMATED_WEIGHT.numBytes(); + } + } + +} diff --git a/atlasdb-cassandra/src/test/java/com/palantir/atlasdb/ThriftObjectSizeUtilsTest.java b/atlasdb-cassandra/src/test/java/com/palantir/atlasdb/ThriftObjectSizeUtilsTest.java index a9f623361e0..3018385043f 100644 --- a/atlasdb-cassandra/src/test/java/com/palantir/atlasdb/ThriftObjectSizeUtilsTest.java +++ b/atlasdb-cassandra/src/test/java/com/palantir/atlasdb/ThriftObjectSizeUtilsTest.java @@ -19,6 +19,8 @@ import static org.assertj.core.api.Assertions.assertThat; import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; import org.apache.cassandra.thrift.Column; import org.apache.cassandra.thrift.ColumnOrSuperColumn; @@ -33,15 +35,18 @@ import org.junit.Test; import com.google.common.collect.ImmutableList; -import com.palantir.atlasdb.keyvalue.cassandra.ThriftObjectSizeUtils; +import com.google.common.collect.ImmutableMap; +import com.palantir.atlasdb.keyvalue.cassandra.qos.ThriftObjectSizeUtils; public class ThriftObjectSizeUtilsTest { - private static final String TEST_MAME = "test"; - private static final Column TEST_COLUMN = new Column(ByteBuffer.wrap(TEST_MAME.getBytes())); + private static final String TEST_MAME = "foo"; + private static final ByteBuffer TEST_NAME_BYTES = ByteBuffer.wrap(TEST_MAME.getBytes()); + private static final Column TEST_COLUMN = new Column(TEST_NAME_BYTES); - - private static final long TEST_COLUMN_SIZE = 4L + TEST_MAME.getBytes().length + 4L + 8L; + private static final long TEST_NAME_SIZE = 3L; + private static final long TEST_NAME_BYTES_SIZE = TEST_NAME_BYTES.remaining(); + private static final long TEST_COLUMN_SIZE = TEST_NAME_BYTES_SIZE + 4L + 4L + 8L; private static final ColumnOrSuperColumn EMPTY_COLUMN_OR_SUPERCOLUMN = new ColumnOrSuperColumn(); private static final long EMPTY_COLUMN_OR_SUPERCOLUMN_SIZE = Integer.BYTES * 4; @@ -74,7 +79,7 @@ public void getSizeForColumnOrSuperColumnWithANonEmptyColumnAndSuperColumn() { .setColumn(TEST_COLUMN) .setSuper_column(new SuperColumn(ByteBuffer.wrap(TEST_MAME.getBytes()), ImmutableList.of(TEST_COLUMN))))) - .isEqualTo(Integer.BYTES * 2 + TEST_COLUMN_SIZE + TEST_MAME.getBytes().length + TEST_COLUMN_SIZE); + .isEqualTo(Integer.BYTES * 2 + TEST_COLUMN_SIZE + TEST_NAME_BYTES_SIZE + TEST_COLUMN_SIZE); } @Test @@ -198,4 +203,61 @@ public void getSizeForKeySliceWithKeyAndColumns() { .setColumns(ImmutableList.of(EMPTY_COLUMN_OR_SUPERCOLUMN)))) .isEqualTo(TEST_MAME.getBytes().length + EMPTY_COLUMN_OR_SUPERCOLUMN_SIZE); } + + @Test + public void getSizeForBatchMutate() { + Map>> batchMutateMap = ImmutableMap.of( + TEST_NAME_BYTES, + ImmutableMap.of( + TEST_MAME, + ImmutableList.of(new Mutation().setColumn_or_supercolumn(EMPTY_COLUMN_OR_SUPERCOLUMN)))); + + long expectedSize = TEST_NAME_BYTES_SIZE + + TEST_NAME_SIZE + + Integer.BYTES + + EMPTY_COLUMN_OR_SUPERCOLUMN_SIZE; + + assertThat(ThriftObjectSizeUtils.getApproximateWriteByteCount(batchMutateMap)).isEqualTo(expectedSize); + } + + @Test + public void getStringSize() { + assertThat(ThriftObjectSizeUtils.getStringSize(TEST_MAME)).isEqualTo(TEST_NAME_SIZE); + } + + @Test + public void getMultigetResultSize() { + Map> result = ImmutableMap.of( + TEST_NAME_BYTES, ImmutableList.of(EMPTY_COLUMN_OR_SUPERCOLUMN)); + + long expectedSize = TEST_NAME_BYTES_SIZE + + EMPTY_COLUMN_OR_SUPERCOLUMN_SIZE; + + assertThat(ThriftObjectSizeUtils.getApproximateReadByteCount(result)).isEqualTo(expectedSize); + } + + @Test + public void getKeySlicesSize() { + List slices = ImmutableList.of( + new KeySlice() + .setKey(TEST_NAME_BYTES) + .setColumns(ImmutableList.of(EMPTY_COLUMN_OR_SUPERCOLUMN))); + + long expectedSize = TEST_NAME_BYTES_SIZE + + EMPTY_COLUMN_OR_SUPERCOLUMN_SIZE; + + assertThat(ThriftObjectSizeUtils.getApproximateReadByteCount(slices)).isEqualTo(expectedSize); + } + + @Test + public void getCasSize() { + List columns = ImmutableList.of( + TEST_COLUMN, + TEST_COLUMN); + + long expectedSize = TEST_COLUMN_SIZE * 2; + + assertThat(ThriftObjectSizeUtils.getCasByteCount(columns)).isEqualTo(expectedSize); + } + } diff --git a/atlasdb-cassandra/src/test/java/com/palantir/atlasdb/keyvalue/cassandra/QosCassandraClientTest.java b/atlasdb-cassandra/src/test/java/com/palantir/atlasdb/keyvalue/cassandra/QosCassandraClientTest.java index 3a0280c793e..a3b56f37c59 100644 --- a/atlasdb-cassandra/src/test/java/com/palantir/atlasdb/keyvalue/cassandra/QosCassandraClientTest.java +++ b/atlasdb-cassandra/src/test/java/com/palantir/atlasdb/keyvalue/cassandra/QosCassandraClientTest.java @@ -16,6 +16,7 @@ package com.palantir.atlasdb.keyvalue.cassandra; +import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -25,6 +26,7 @@ import javax.naming.LimitExceededException; +import org.apache.cassandra.thrift.Column; import org.apache.cassandra.thrift.Compression; import org.apache.cassandra.thrift.ConsistencyLevel; import org.apache.cassandra.thrift.KeyRange; @@ -37,6 +39,7 @@ import com.google.common.collect.ImmutableMap; import com.palantir.atlasdb.encoding.PtBytes; import com.palantir.atlasdb.keyvalue.api.TableReference; +import com.palantir.atlasdb.keyvalue.cassandra.qos.QosCassandraClient; import com.palantir.atlasdb.keyvalue.cassandra.thrift.SlicePredicates; import com.palantir.atlasdb.qos.QosClient; @@ -61,7 +64,7 @@ public void setUp() { public void multigetSliceChecksLimit() throws TException, LimitExceededException { client.multiget_slice("get", TEST_TABLE, ImmutableList.of(ROW_KEY), SLICE_PREDICATE, ConsistencyLevel.ANY); - verify(qosClient, times(1)).checkLimit(); + verify(qosClient, times(1)).executeRead(any(), any()); verifyNoMoreInteractions(qosClient); } @@ -69,7 +72,7 @@ public void multigetSliceChecksLimit() throws TException, LimitExceededException public void batchMutateChecksLimit() throws TException, LimitExceededException { client.batch_mutate("put", ImmutableMap.of(), ConsistencyLevel.ANY); - verify(qosClient, times(1)).checkLimit(); + verify(qosClient, times(1)).executeWrite(any(), any()); verifyNoMoreInteractions(qosClient); } @@ -78,7 +81,7 @@ public void executeCqlQueryChecksLimit() throws TException, LimitExceededExcepti CqlQuery query = new CqlQuery("SELECT * FROM test_table LIMIT 1"); client.execute_cql3_query(query, Compression.NONE, ConsistencyLevel.ANY); - verify(qosClient, times(1)).checkLimit(); + verify(qosClient, times(1)).executeRead(any(), any()); verifyNoMoreInteractions(qosClient); } @@ -86,7 +89,15 @@ public void executeCqlQueryChecksLimit() throws TException, LimitExceededExcepti public void getRangeSlicesChecksLimit() throws TException, LimitExceededException { client.get_range_slices("get", TEST_TABLE, SLICE_PREDICATE, new KeyRange(), ConsistencyLevel.ANY); - verify(qosClient, times(1)).checkLimit(); + verify(qosClient, times(1)).executeRead(any(), any()); + verifyNoMoreInteractions(qosClient); + } + + @Test + public void casDoesNotCheckLimit() throws TException, LimitExceededException { + client.cas(TEST_TABLE, ByteBuffer.allocate(1), ImmutableList.of(new Column()), ImmutableList.of(new Column()), + ConsistencyLevel.SERIAL, ConsistencyLevel.SERIAL); + verifyNoMoreInteractions(qosClient); } } diff --git a/atlasdb-cassandra/src/test/java/com/palantir/atlasdb/keyvalue/cassandra/qos/ThriftQueryWeighersTest.java b/atlasdb-cassandra/src/test/java/com/palantir/atlasdb/keyvalue/cassandra/qos/ThriftQueryWeighersTest.java new file mode 100644 index 00000000000..6e28e848cdc --- /dev/null +++ b/atlasdb-cassandra/src/test/java/com/palantir/atlasdb/keyvalue/cassandra/qos/ThriftQueryWeighersTest.java @@ -0,0 +1,149 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the BSD-3 License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/BSD-3-Clause + * + * 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 com.palantir.atlasdb.keyvalue.cassandra.qos; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; + +import org.apache.cassandra.thrift.Column; +import org.apache.cassandra.thrift.ColumnOrSuperColumn; +import org.apache.cassandra.thrift.CqlResult; +import org.apache.cassandra.thrift.KeySlice; +import org.apache.cassandra.thrift.Mutation; +import org.junit.Test; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.palantir.atlasdb.qos.ImmutableQueryWeight; +import com.palantir.atlasdb.qos.QosClient; +import com.palantir.atlasdb.qos.QueryWeight; + +public class ThriftQueryWeighersTest { + + private static final ByteBuffer BYTES1 = ByteBuffer.allocate(3); + private static final ByteBuffer BYTES2 = ByteBuffer.allocate(7); + private static final ColumnOrSuperColumn COLUMN_OR_SUPER = new ColumnOrSuperColumn(); + private static final Column COLUMN = new Column(); + private static final KeySlice KEY_SLICE = new KeySlice(); + private static final Mutation MUTATION = new Mutation(); + + private static final long TIME_TAKEN = 123L; + + private static final QueryWeight DEFAULT_WEIGHT = ImmutableQueryWeight.builder() + .from(ThriftQueryWeighers.DEFAULT_ESTIMATED_WEIGHT) + .timeTakenNanos(TIME_TAKEN) + .build(); + + @Test + public void multigetSliceWeigherReturnsCorrectNumRows() { + Map> result = ImmutableMap.of( + BYTES1, ImmutableList.of(COLUMN_OR_SUPER, COLUMN_OR_SUPER), + BYTES2, ImmutableList.of(COLUMN_OR_SUPER)); + + long actualNumRows = ThriftQueryWeighers.MULTIGET_SLICE.weighSuccess(result, TIME_TAKEN).numDistinctRows(); + + assertThat(actualNumRows).isEqualTo(2); + } + + @Test + public void rangeSlicesWeigherReturnsCorrectNumRows() { + List result = ImmutableList.of(KEY_SLICE, KEY_SLICE, KEY_SLICE); + + long actualNumRows = ThriftQueryWeighers.GET_RANGE_SLICES.weighSuccess(result, TIME_TAKEN).numDistinctRows(); + + assertThat(actualNumRows).isEqualTo(3); + } + + @Test + public void getWeigherReturnsCorrectNumRows() { + long actualNumRows = ThriftQueryWeighers.GET.weighSuccess(COLUMN_OR_SUPER, TIME_TAKEN).numDistinctRows(); + + assertThat(actualNumRows).isEqualTo(1); + } + + @Test + public void executeCql3QueryWeigherReturnsOneRowAlways() { + long actualNumRows = ThriftQueryWeighers.EXECUTE_CQL3_QUERY.weighSuccess(new CqlResult(), + TIME_TAKEN).numDistinctRows(); + + assertThat(actualNumRows).isEqualTo(1); + } + + @Test + public void batchMutateWeigherReturnsCorrectNumRows() { + Map>> mutations = ImmutableMap.of( + BYTES1, ImmutableMap.of( + "table1", ImmutableList.of(MUTATION, MUTATION), + "table2", ImmutableList.of(MUTATION)), + BYTES2, ImmutableMap.of( + "table1", ImmutableList.of(MUTATION))); + + long actualNumRows = ThriftQueryWeighers.batchMutate(mutations).weighSuccess(null, TIME_TAKEN) + .numDistinctRows(); + + assertThat(actualNumRows).isEqualTo(2); + } + + @Test + public void multigetSliceWeigherReturnsDefaultEstimateForFailure() { + QueryWeight weight = ThriftQueryWeighers.MULTIGET_SLICE.weighFailure(new RuntimeException(), TIME_TAKEN); + + assertThat(weight).isEqualTo(DEFAULT_WEIGHT); + } + + @Test + public void getWeigherReturnsDefaultEstimateForFailure() { + QueryWeight weight = ThriftQueryWeighers.GET.weighFailure(new RuntimeException(), TIME_TAKEN); + + assertThat(weight).isEqualTo(DEFAULT_WEIGHT); + } + + @Test + public void getRangeSlicesWeigherReturnsDefaultEstimateForFailure() { + QueryWeight weight = ThriftQueryWeighers.GET_RANGE_SLICES.weighFailure(new RuntimeException(), TIME_TAKEN); + + assertThat(weight).isEqualTo(DEFAULT_WEIGHT); + } + + @Test + public void batchMutateWeigherReturnsEstimateForFailure() { + Map>> mutations = ImmutableMap.of( + BYTES1, ImmutableMap.of("foo", ImmutableList.of(MUTATION, MUTATION))); + + QosClient.QueryWeigher weigher = ThriftQueryWeighers.batchMutate(mutations); + + QueryWeight expected = ImmutableQueryWeight.builder() + .from(weigher.estimate()) + .timeTakenNanos(TIME_TAKEN) + .build(); + QueryWeight actual = weigher.weighFailure(new RuntimeException(), TIME_TAKEN); + + assertThat(actual).isEqualTo(expected); + } + + @Test + public void cql3QueryWeigherReturnsDefaultEstimateForFailure() { + QueryWeight weight = ThriftQueryWeighers.EXECUTE_CQL3_QUERY.weighFailure(new RuntimeException(), + TIME_TAKEN); + + assertThat(weight).isEqualTo(DEFAULT_WEIGHT); + } + +} diff --git a/atlasdb-cli-distribution/versions.lock b/atlasdb-cli-distribution/versions.lock index 828f6c32d38..2de3611ad82 100644 --- a/atlasdb-cli-distribution/versions.lock +++ b/atlasdb-cli-distribution/versions.lock @@ -380,6 +380,7 @@ "com.palantir.atlasdb:atlasdb-dbkvs", "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:atlasdb-jdbc", + "com.palantir.atlasdb:qos-service-impl", "com.palantir.atlasdb:timestamp-impl" ] }, @@ -544,6 +545,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, diff --git a/atlasdb-cli/versions.lock b/atlasdb-cli/versions.lock index bff4b7e79a3..b845b6e0a40 100644 --- a/atlasdb-cli/versions.lock +++ b/atlasdb-cli/versions.lock @@ -343,6 +343,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-cassandra", "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl", "com.palantir.atlasdb:timestamp-impl" ] }, @@ -470,6 +471,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, @@ -1344,6 +1346,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-cassandra", "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl", "com.palantir.atlasdb:timestamp-impl" ] }, @@ -1471,6 +1474,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, diff --git a/atlasdb-client/src/main/java/com/palantir/atlasdb/qos/ratelimit/QosAwareThrowables.java b/atlasdb-client/src/main/java/com/palantir/atlasdb/qos/ratelimit/QosAwareThrowables.java new file mode 100644 index 00000000000..53823b5dfd2 --- /dev/null +++ b/atlasdb-client/src/main/java/com/palantir/atlasdb/qos/ratelimit/QosAwareThrowables.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the BSD-3 License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/BSD-3-Clause + * + * 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 com.palantir.atlasdb.qos.ratelimit; + +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.ExecutionException; + +import com.palantir.common.base.Throwables; + +public final class QosAwareThrowables { + private QosAwareThrowables() { + // no + } + + /** + * If the provided Throwable is + * a) an ExecutionException or InvocationTargetException, then apply this method on the cause; + * b) a RateLimitExceededException or an AtlasDbDependencyException, then rethrow it; + * c) none of the above, then wrap it in an AtlasDbDependencyException and throw that. + */ + public static RuntimeException unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException(Throwable ex) { + if (ex instanceof ExecutionException || ex instanceof InvocationTargetException) { + // Needs to be this way in case you have ITE(RLE) or variants of that. + throw unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException(ex.getCause()); + } else if (ex instanceof RateLimitExceededException) { + throw (RateLimitExceededException) ex; + } + throw Throwables.unwrapAndThrowAtlasDbDependencyException(ex); + } + +} diff --git a/atlasdb-client/src/test/java/com/palantir/atlasdb/qos/ratelimit/QosAwareThrowablesTest.java b/atlasdb-client/src/test/java/com/palantir/atlasdb/qos/ratelimit/QosAwareThrowablesTest.java new file mode 100644 index 00000000000..2abad554d33 --- /dev/null +++ b/atlasdb-client/src/test/java/com/palantir/atlasdb/qos/ratelimit/QosAwareThrowablesTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the BSD-3 License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/BSD-3-Clause + * + * 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 com.palantir.atlasdb.qos.ratelimit; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.ExecutionException; + +import org.junit.Test; + +import com.palantir.common.exception.AtlasDbDependencyException; + +public class QosAwareThrowablesTest { + private static final Exception RATE_LIMIT_EXCEEDED_EXCEPTION = + new RateLimitExceededException("Stop!"); + private static final Exception ATLASDB_DEPENDENCY_EXCEPTION = + new AtlasDbDependencyException("The TimeLock is dead, long live the TimeLock"); + + @Test + public void unwrapAndThrowRateLimitExceededOrAtlasDbDependencyExceptionCanThrowRateLimitExceededException() { + assertThatThrownBy(() -> QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException( + RATE_LIMIT_EXCEEDED_EXCEPTION)).isEqualTo(RATE_LIMIT_EXCEEDED_EXCEPTION); + } + + @Test + public void unwrapAndThrowRateLimitExceededOrAtlasDbDependencyExceptionCanThrowAtlasDbDependencyException() { + assertThatThrownBy(() -> QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException( + ATLASDB_DEPENDENCY_EXCEPTION)).isEqualTo(ATLASDB_DEPENDENCY_EXCEPTION); + } + + @Test + public void unwrapAndThrowRateLimitExceededOrAtlasDbDependencyExceptionThrowsWrappedRateLimitExceededExceptions() { + assertThatThrownBy(() -> QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException( + new ExecutionException(RATE_LIMIT_EXCEEDED_EXCEPTION))).isEqualTo(RATE_LIMIT_EXCEEDED_EXCEPTION); + assertThatThrownBy(() -> QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException( + new InvocationTargetException(RATE_LIMIT_EXCEEDED_EXCEPTION))).isEqualTo(RATE_LIMIT_EXCEEDED_EXCEPTION); + } + + @Test + public void unwrapAndThrowRateLimitExceededOrAtlasDbDependencyExceptionThrowsWrappedAtlasDbDependencyExceptions() { + assertThatThrownBy(() -> QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException( + new ExecutionException(ATLASDB_DEPENDENCY_EXCEPTION))).isEqualTo(ATLASDB_DEPENDENCY_EXCEPTION); + assertThatThrownBy(() -> QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException( + new InvocationTargetException(ATLASDB_DEPENDENCY_EXCEPTION))).isEqualTo(ATLASDB_DEPENDENCY_EXCEPTION); + } + + @Test + public void unwrapAndThrowRateLimitExceededOrAtlasDbDependencyExceptionWrapsRuntimeExceptions() { + assertThatThrownBy(() -> QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException( + new RuntimeException("runtimeException"))).isInstanceOf(AtlasDbDependencyException.class); + } + + @Test + public void unwrapAndThrowRateLimitExceededOrAtlasDbDependencyExceptionWrapsCheckedExceptions() { + assertThatThrownBy(() -> QosAwareThrowables.unwrapAndThrowRateLimitExceededOrAtlasDbDependencyException( + new IOException("ioException"))).isInstanceOf(AtlasDbDependencyException.class); + } +} diff --git a/atlasdb-commons/build.gradle b/atlasdb-commons/build.gradle index 5f19012ad2f..8a4fb6c7be3 100644 --- a/atlasdb-commons/build.gradle +++ b/atlasdb-commons/build.gradle @@ -3,15 +3,16 @@ apply from: "../gradle/publish-jars.gradle" apply from: "../gradle/shared.gradle" dependencies { - compile group: 'com.google.code.findbugs', name: 'jsr305' - compile group: 'com.google.guava', name: 'guava' - compile group: 'org.slf4j', name: 'slf4j-api' compile project(":commons-executors") - compile group: 'javax.ws.rs', name: 'javax.ws.rs-api' + compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations' + compile group: 'com.google.code.findbugs', name: 'jsr305' + compile group: 'com.google.guava', name: 'guava' + compile group: 'com.palantir.safe-logging', name: 'safe-logging' compile group: 'io.dropwizard.metrics', name: 'metrics-core' + compile group: 'javax.ws.rs', name: 'javax.ws.rs-api' compile group: 'net.jpountz.lz4', name: 'lz4' - compile group: 'com.palantir.safe-logging', name: 'safe-logging' + compile group: 'org.slf4j', name: 'slf4j-api' testCompile group: 'junit', name: 'junit' testCompile group: 'org.assertj', name: 'assertj-core' diff --git a/atlasdb-commons/src/test/java/com/palantir/common/base/ThrowablesTest.java b/atlasdb-commons/src/test/java/com/palantir/common/base/ThrowablesTest.java index 1b3f9f7ad6f..157054a06ba 100644 --- a/atlasdb-commons/src/test/java/com/palantir/common/base/ThrowablesTest.java +++ b/atlasdb-commons/src/test/java/com/palantir/common/base/ThrowablesTest.java @@ -23,7 +23,6 @@ import org.junit.Test; public class ThrowablesTest extends Assert { - @Before public void setUp() throws Exception { NoUsefulConstructorException.noUsefulConstructorCalled = false; @@ -31,7 +30,6 @@ public void setUp() throws Exception { @Test public void testRewrap() { - try { throwTwoArgConstructorException(); fail("Should not get here"); @@ -61,9 +59,9 @@ public void testRewrap() { int sizeAfter = e.getStackTrace().length; assertTrue(sizeAfter + " should be > " + sizeBefore, sizeAfter > sizeBefore); } - } + // only has a (string, throwable) constructor public void throwTwoArgConstructorException() throws TwoArgConstructorException { throw new TwoArgConstructorException("Told you so", new IOException("Contained")); diff --git a/atlasdb-config/src/main/java/com/palantir/atlasdb/config/AtlasDbRuntimeConfig.java b/atlasdb-config/src/main/java/com/palantir/atlasdb/config/AtlasDbRuntimeConfig.java index 30ca96ce1cc..df5fbf01319 100644 --- a/atlasdb-config/src/main/java/com/palantir/atlasdb/config/AtlasDbRuntimeConfig.java +++ b/atlasdb-config/src/main/java/com/palantir/atlasdb/config/AtlasDbRuntimeConfig.java @@ -23,7 +23,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.palantir.atlasdb.AtlasDbConstants; -import com.palantir.remoting.api.config.service.ServiceConfiguration; +import com.palantir.atlasdb.qos.config.QosClientConfig; @JsonDeserialize(as = ImmutableAtlasDbRuntimeConfig.class) @JsonSerialize(as = ImmutableAtlasDbRuntimeConfig.class) @@ -61,7 +61,10 @@ public long getTimestampCacheSize() { return AtlasDbConstants.DEFAULT_TIMESTAMP_CACHE_SIZE; } - public abstract Optional getQosServiceConfiguration(); + @Value.Default + public QosClientConfig qos() { + return QosClientConfig.DEFAULT; + } /** * Runtime live-reloadable parameters for communicating with TimeLock. diff --git a/atlasdb-config/src/main/java/com/palantir/atlasdb/factory/ServiceDiscoveringAtlasSupplier.java b/atlasdb-config/src/main/java/com/palantir/atlasdb/factory/ServiceDiscoveringAtlasSupplier.java index 180551f6e89..bbb63f61056 100644 --- a/atlasdb-config/src/main/java/com/palantir/atlasdb/factory/ServiceDiscoveringAtlasSupplier.java +++ b/atlasdb-config/src/main/java/com/palantir/atlasdb/factory/ServiceDiscoveringAtlasSupplier.java @@ -59,7 +59,7 @@ public class ServiceDiscoveringAtlasSupplier { public ServiceDiscoveringAtlasSupplier(KeyValueServiceConfig config, Optional leaderConfig) { this(config, leaderConfig, Optional.empty(), AtlasDbConstants.DEFAULT_INITIALIZE_ASYNC, - FakeQosClient.getDefault()); + FakeQosClient.INSTANCE); } public ServiceDiscoveringAtlasSupplier( @@ -68,7 +68,7 @@ public ServiceDiscoveringAtlasSupplier( Optional namespace, Optional timestampTable) { this(config, leaderConfig, namespace, timestampTable, AtlasDbConstants.DEFAULT_INITIALIZE_ASYNC, - FakeQosClient.getDefault()); + FakeQosClient.INSTANCE); } public ServiceDiscoveringAtlasSupplier( diff --git a/atlasdb-config/src/main/java/com/palantir/atlasdb/factory/TransactionManagers.java b/atlasdb-config/src/main/java/com/palantir/atlasdb/factory/TransactionManagers.java index c1dd7003aaf..94359348491 100644 --- a/atlasdb-config/src/main/java/com/palantir/atlasdb/factory/TransactionManagers.java +++ b/atlasdb-config/src/main/java/com/palantir/atlasdb/factory/TransactionManagers.java @@ -18,8 +18,6 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Supplier; @@ -30,7 +28,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.codahale.metrics.InstrumentedScheduledExecutorService; import com.codahale.metrics.MetricRegistry; import com.fasterxml.jackson.annotation.JsonIgnore; import com.google.common.annotations.VisibleForTesting; @@ -72,11 +69,10 @@ import com.palantir.atlasdb.persistentlock.KvsBackedPersistentLockService; import com.palantir.atlasdb.persistentlock.NoOpPersistentLockService; import com.palantir.atlasdb.persistentlock.PersistentLockService; -import com.palantir.atlasdb.qos.FakeQosClient; import com.palantir.atlasdb.qos.QosClient; -import com.palantir.atlasdb.qos.QosService; import com.palantir.atlasdb.qos.client.AtlasDbQosClient; -import com.palantir.atlasdb.qos.ratelimit.QosRateLimiter; +import com.palantir.atlasdb.qos.config.QosClientConfig; +import com.palantir.atlasdb.qos.ratelimit.QosRateLimiters; import com.palantir.atlasdb.schema.generated.SweepTableFactory; import com.palantir.atlasdb.spi.AtlasDbFactory; import com.palantir.atlasdb.spi.KeyValueServiceConfig; @@ -103,6 +99,7 @@ import com.palantir.atlasdb.transaction.service.TransactionService; import com.palantir.atlasdb.transaction.service.TransactionServices; import com.palantir.atlasdb.util.AtlasDbMetrics; +import com.palantir.atlasdb.util.JavaSuppliers; import com.palantir.leader.LeaderElectionService; import com.palantir.leader.PingableLeader; import com.palantir.leader.proxy.AwaitingLeadershipProxy; @@ -117,9 +114,6 @@ import com.palantir.lock.impl.LockServiceImpl; import com.palantir.lock.v2.TimelockService; import com.palantir.logsafe.UnsafeArg; -import com.palantir.remoting.api.config.service.ServiceConfiguration; -import com.palantir.remoting3.clients.ClientConfigurations; -import com.palantir.remoting3.jaxrs.JaxRsClient; import com.palantir.timestamp.TimestampService; import com.palantir.timestamp.TimestampStoreInvalidator; import com.palantir.util.OptionalResolver; @@ -316,8 +310,7 @@ SerializableTransactionManager serializable() { java.util.function.Supplier runtimeConfigSupplier = () -> runtimeConfigSupplier().get().orElse(defaultRuntime); - - QosClient qosClient = getQosClient(runtimeConfigSupplier.get().getQosServiceConfiguration()); + QosClient qosClient = getQosClient(JavaSuppliers.compose(conf -> conf.qos(), runtimeConfigSupplier)); ServiceDiscoveringAtlasSupplier atlasFactory = new ServiceDiscoveringAtlasSupplier( @@ -411,19 +404,11 @@ SerializableTransactionManager serializable() { return transactionManager; } - private QosClient getQosClient(Optional serviceConfiguration) { - return serviceConfiguration.map(this::createAtlasDbQosClient).orElseGet(FakeQosClient::getDefault); - } - - private QosClient createAtlasDbQosClient(ServiceConfiguration serviceConfiguration) { - QosService qosService = JaxRsClient.create(QosService.class, - userAgent(), - ClientConfigurations.of(serviceConfiguration)); - ScheduledExecutorService scheduler = new InstrumentedScheduledExecutorService( - Executors.newSingleThreadScheduledExecutor(), - AtlasDbMetrics.getMetricRegistry(), - "qos-client-executor"); - return new AtlasDbQosClient(qosService, scheduler, config().getNamespaceString(), QosRateLimiter.create()); + private QosClient getQosClient(Supplier config) { + QosRateLimiters rateLimiters = QosRateLimiters.create( + JavaSuppliers.compose(conf -> conf.maxBackoffSleepTime().toMilliseconds(), config), + JavaSuppliers.compose(QosClientConfig::limits, config)); + return AtlasDbQosClient.create(rateLimiters); } private static boolean areTransactionManagerInitializationPrerequisitesSatisfied( diff --git a/atlasdb-config/src/main/java/com/palantir/atlasdb/http/RateLimitExceededExceptionMapper.java b/atlasdb-config/src/main/java/com/palantir/atlasdb/http/RateLimitExceededExceptionMapper.java new file mode 100644 index 00000000000..ed531797256 --- /dev/null +++ b/atlasdb-config/src/main/java/com/palantir/atlasdb/http/RateLimitExceededExceptionMapper.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the BSD-3 License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/BSD-3-Clause + * + * 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 com.palantir.atlasdb.http; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import com.palantir.atlasdb.qos.ratelimit.RateLimitExceededException; + +/** + * This class maps instances of {@link RateLimitExceededException} to HTTP responses with a 429 status code. + * Users may register this exception mapper for a standard way of converting these exceptions to 429 responses, which + * are meaningful in that {@link com.palantir.remoting3.jaxrs.JaxRsClient}s are able to back off accordingly. + */ +public class RateLimitExceededExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(RateLimitExceededException exception) { + return ExceptionMappers.encodeExceptionResponse(exception, 429).build(); + } +} diff --git a/atlasdb-config/src/test/java/com/palantir/atlasdb/factory/TransactionManagersTest.java b/atlasdb-config/src/test/java/com/palantir/atlasdb/factory/TransactionManagersTest.java index 729b4f0e69f..5a6258b43bd 100644 --- a/atlasdb-config/src/test/java/com/palantir/atlasdb/factory/TransactionManagersTest.java +++ b/atlasdb-config/src/test/java/com/palantir/atlasdb/factory/TransactionManagersTest.java @@ -73,6 +73,7 @@ import com.palantir.atlasdb.config.TimeLockClientConfig; import com.palantir.atlasdb.factory.startup.TimeLockMigrator; import com.palantir.atlasdb.memory.InMemoryAtlasDbConfig; +import com.palantir.atlasdb.qos.config.QosClientConfig; import com.palantir.atlasdb.table.description.GenericTestSchema; import com.palantir.atlasdb.transaction.impl.SerializableTransactionManager; import com.palantir.atlasdb.util.MetricsRule; @@ -193,7 +194,7 @@ public void setup() throws JsonProcessingException { runtimeConfig = mock(AtlasDbRuntimeConfig.class); when(runtimeConfig.timestampClient()).thenReturn(ImmutableTimestampClientConfig.of(false)); - when(runtimeConfig.getQosServiceConfiguration()).thenReturn(Optional.empty()); + when(runtimeConfig.qos()).thenReturn(QosClientConfig.DEFAULT); when(runtimeConfig.timelockRuntime()).thenReturn(Optional.empty()); environment = mock(Consumer.class); diff --git a/atlasdb-config/src/test/java/com/palantir/atlasdb/http/RateLimitExceededExceptionMapperTest.java b/atlasdb-config/src/test/java/com/palantir/atlasdb/http/RateLimitExceededExceptionMapperTest.java new file mode 100644 index 00000000000..d4e144e212d --- /dev/null +++ b/atlasdb-config/src/test/java/com/palantir/atlasdb/http/RateLimitExceededExceptionMapperTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the BSD-3 License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/BSD-3-Clause + * + * 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 com.palantir.atlasdb.http; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +import com.palantir.atlasdb.qos.ratelimit.RateLimitExceededException; + +public class RateLimitExceededExceptionMapperTest { + @Test + public void responsesHaveErrorCode429() { + RateLimitExceededException rateLimitExceededException = new RateLimitExceededException("Stop!"); + assertThat(new RateLimitExceededExceptionMapper().toResponse(rateLimitExceededException).getStatus()) + .isEqualTo(429); + } +} diff --git a/atlasdb-config/versions.lock b/atlasdb-config/versions.lock index a6177b13f9c..0a6d6a960df 100644 --- a/atlasdb-config/versions.lock +++ b/atlasdb-config/versions.lock @@ -292,7 +292,8 @@ "com.palantir.atlasdb:atlasdb-client": { "project": true, "transitive": [ - "com.palantir.atlasdb:atlasdb-impl-shared" + "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl" ] }, "com.palantir.atlasdb:atlasdb-client-protobufs": { @@ -372,6 +373,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, @@ -1047,7 +1049,8 @@ "com.palantir.atlasdb:atlasdb-client": { "project": true, "transitive": [ - "com.palantir.atlasdb:atlasdb-impl-shared" + "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl" ] }, "com.palantir.atlasdb:atlasdb-client-protobufs": { @@ -1127,6 +1130,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, diff --git a/atlasdb-console-distribution/versions.lock b/atlasdb-console-distribution/versions.lock index 16d9e08488b..32099b9518d 100644 --- a/atlasdb-console-distribution/versions.lock +++ b/atlasdb-console-distribution/versions.lock @@ -371,6 +371,7 @@ "com.palantir.atlasdb:atlasdb-dbkvs", "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:atlasdb-jdbc", + "com.palantir.atlasdb:qos-service-impl", "com.palantir.atlasdb:timestamp-impl" ] }, @@ -526,6 +527,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, diff --git a/atlasdb-console/versions.lock b/atlasdb-console/versions.lock index 914f308086e..f49d50458e0 100644 --- a/atlasdb-console/versions.lock +++ b/atlasdb-console/versions.lock @@ -300,7 +300,8 @@ "com.palantir.atlasdb:atlasdb-client": { "project": true, "transitive": [ - "com.palantir.atlasdb:atlasdb-impl-shared" + "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl" ] }, "com.palantir.atlasdb:atlasdb-client-protobufs": { @@ -401,6 +402,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, @@ -1130,7 +1132,8 @@ "com.palantir.atlasdb:atlasdb-client": { "project": true, "transitive": [ - "com.palantir.atlasdb:atlasdb-impl-shared" + "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl" ] }, "com.palantir.atlasdb:atlasdb-client-protobufs": { @@ -1231,6 +1234,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, diff --git a/atlasdb-dagger/src/main/java/com/palantir/atlasdb/services/ServicesConfig.java b/atlasdb-dagger/src/main/java/com/palantir/atlasdb/services/ServicesConfig.java index f25bc8dd431..eeafa07a2dc 100644 --- a/atlasdb-dagger/src/main/java/com/palantir/atlasdb/services/ServicesConfig.java +++ b/atlasdb-dagger/src/main/java/com/palantir/atlasdb/services/ServicesConfig.java @@ -43,7 +43,7 @@ public ServiceDiscoveringAtlasSupplier atlasDbSupplier() { atlasDbConfig().leader(), atlasDbConfig().namespace(), atlasDbConfig().initializeAsync(), - FakeQosClient.getDefault()); + FakeQosClient.INSTANCE); } @Value.Default diff --git a/atlasdb-dagger/versions.lock b/atlasdb-dagger/versions.lock index 7134077d838..0761be86cc6 100644 --- a/atlasdb-dagger/versions.lock +++ b/atlasdb-dagger/versions.lock @@ -303,7 +303,8 @@ "com.palantir.atlasdb:atlasdb-client": { "project": true, "transitive": [ - "com.palantir.atlasdb:atlasdb-impl-shared" + "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl" ] }, "com.palantir.atlasdb:atlasdb-client-protobufs": { @@ -404,6 +405,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, @@ -1123,7 +1125,8 @@ "com.palantir.atlasdb:atlasdb-client": { "project": true, "transitive": [ - "com.palantir.atlasdb:atlasdb-impl-shared" + "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl" ] }, "com.palantir.atlasdb:atlasdb-client-protobufs": { @@ -1224,6 +1227,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, diff --git a/atlasdb-dbkvs-tests/versions.lock b/atlasdb-dbkvs-tests/versions.lock index c3b4d00ddd3..4011b6c017b 100644 --- a/atlasdb-dbkvs-tests/versions.lock +++ b/atlasdb-dbkvs-tests/versions.lock @@ -368,7 +368,8 @@ "project": true, "transitive": [ "com.palantir.atlasdb:atlasdb-api", - "com.palantir.atlasdb:atlasdb-client" + "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared" ] }, "com.palantir.atlasdb:timestamp-api": { @@ -1182,7 +1183,8 @@ "project": true, "transitive": [ "com.palantir.atlasdb:atlasdb-api", - "com.palantir.atlasdb:atlasdb-client" + "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared" ] }, "com.palantir.atlasdb:timestamp-api": { diff --git a/atlasdb-dbkvs/versions.lock b/atlasdb-dbkvs/versions.lock index 0d2c29c17d8..e858d2e15ee 100644 --- a/atlasdb-dbkvs/versions.lock +++ b/atlasdb-dbkvs/versions.lock @@ -342,7 +342,8 @@ "project": true, "transitive": [ "com.palantir.atlasdb:atlasdb-api", - "com.palantir.atlasdb:atlasdb-client" + "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared" ] }, "com.palantir.atlasdb:timestamp-api": { @@ -1070,7 +1071,8 @@ "project": true, "transitive": [ "com.palantir.atlasdb:atlasdb-api", - "com.palantir.atlasdb:atlasdb-client" + "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared" ] }, "com.palantir.atlasdb:timestamp-api": { diff --git a/atlasdb-dropwizard-bundle/versions.lock b/atlasdb-dropwizard-bundle/versions.lock index 58dd032a0ab..6833ca88d3a 100644 --- a/atlasdb-dropwizard-bundle/versions.lock +++ b/atlasdb-dropwizard-bundle/versions.lock @@ -367,6 +367,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-cassandra", "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl", "com.palantir.atlasdb:timestamp-impl" ] }, @@ -501,6 +502,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, @@ -1816,6 +1818,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-cassandra", "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl", "com.palantir.atlasdb:timestamp-impl" ] }, @@ -1950,6 +1953,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, diff --git a/atlasdb-ete-tests/versions.lock b/atlasdb-ete-tests/versions.lock index 00bb8f05c9f..9ac7ebfb245 100644 --- a/atlasdb-ete-tests/versions.lock +++ b/atlasdb-ete-tests/versions.lock @@ -375,6 +375,7 @@ "com.palantir.atlasdb:atlasdb-cassandra", "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:atlasdb-jdbc", + "com.palantir.atlasdb:qos-service-impl", "com.palantir.atlasdb:timestamp-impl" ] }, @@ -525,6 +526,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, @@ -1908,6 +1910,7 @@ "com.palantir.atlasdb:atlasdb-dbkvs", "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:atlasdb-jdbc", + "com.palantir.atlasdb:qos-service-impl", "com.palantir.atlasdb:timestamp-impl" ] }, @@ -2083,6 +2086,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, diff --git a/atlasdb-impl-shared/build.gradle b/atlasdb-impl-shared/build.gradle index 5a6514aad12..9ae23895a02 100644 --- a/atlasdb-impl-shared/build.gradle +++ b/atlasdb-impl-shared/build.gradle @@ -20,6 +20,8 @@ dependencies { compile project(":timestamp-api") compile project(":timestamp-client") compile project(":atlasdb-client") + compile project(":qos-service-api") + compile 'com.palantir.patches.sourceforge:trove3:' + libVersions.trove compile group: 'com.palantir.common', name: 'streams' diff --git a/atlasdb-jepsen-tests/versions.lock b/atlasdb-jepsen-tests/versions.lock index 53dd704b203..51b6705f4af 100644 --- a/atlasdb-jepsen-tests/versions.lock +++ b/atlasdb-jepsen-tests/versions.lock @@ -299,7 +299,8 @@ "com.palantir.atlasdb:atlasdb-client": { "project": true, "transitive": [ - "com.palantir.atlasdb:atlasdb-impl-shared" + "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl" ] }, "com.palantir.atlasdb:atlasdb-client-protobufs": { @@ -392,6 +393,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, @@ -1106,7 +1108,8 @@ "com.palantir.atlasdb:atlasdb-client": { "project": true, "transitive": [ - "com.palantir.atlasdb:atlasdb-impl-shared" + "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl" ] }, "com.palantir.atlasdb:atlasdb-client-protobufs": { @@ -1199,6 +1202,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, diff --git a/atlasdb-perf/versions.lock b/atlasdb-perf/versions.lock index 823e560e1c9..ef49d3ff5db 100644 --- a/atlasdb-perf/versions.lock +++ b/atlasdb-perf/versions.lock @@ -392,6 +392,7 @@ "com.palantir.atlasdb:atlasdb-cassandra", "com.palantir.atlasdb:atlasdb-dbkvs", "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl", "com.palantir.atlasdb:timestamp-impl" ] }, @@ -544,6 +545,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, @@ -1581,6 +1583,7 @@ "com.palantir.atlasdb:atlasdb-cassandra", "com.palantir.atlasdb:atlasdb-dbkvs", "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl", "com.palantir.atlasdb:timestamp-impl" ] }, @@ -1733,6 +1736,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, diff --git a/atlasdb-service-server/versions.lock b/atlasdb-service-server/versions.lock index ff82b5dba67..0ef73faff89 100644 --- a/atlasdb-service-server/versions.lock +++ b/atlasdb-service-server/versions.lock @@ -319,7 +319,8 @@ "com.palantir.atlasdb:atlasdb-client": { "project": true, "transitive": [ - "com.palantir.atlasdb:atlasdb-impl-shared" + "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl" ] }, "com.palantir.atlasdb:atlasdb-client-protobufs": { @@ -420,6 +421,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, @@ -1602,6 +1604,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-cassandra", "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl", "com.palantir.atlasdb:timestamp-impl" ] }, @@ -1723,6 +1726,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, diff --git a/atlasdb-service/versions.lock b/atlasdb-service/versions.lock index 2fa0cb5920f..b3d8d6db6c1 100644 --- a/atlasdb-service/versions.lock +++ b/atlasdb-service/versions.lock @@ -299,7 +299,8 @@ "com.palantir.atlasdb:atlasdb-client": { "project": true, "transitive": [ - "com.palantir.atlasdb:atlasdb-impl-shared" + "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl" ] }, "com.palantir.atlasdb:atlasdb-client-protobufs": { @@ -392,6 +393,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, @@ -1104,7 +1106,8 @@ "com.palantir.atlasdb:atlasdb-client": { "project": true, "transitive": [ - "com.palantir.atlasdb:atlasdb-impl-shared" + "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl" ] }, "com.palantir.atlasdb:atlasdb-client-protobufs": { @@ -1197,6 +1200,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, diff --git a/atlasdb-tests-shared/versions.lock b/atlasdb-tests-shared/versions.lock index e55e2d9ca40..fab697ee11f 100644 --- a/atlasdb-tests-shared/versions.lock +++ b/atlasdb-tests-shared/versions.lock @@ -297,7 +297,8 @@ "project": true, "transitive": [ "com.palantir.atlasdb:atlasdb-api", - "com.palantir.atlasdb:atlasdb-client" + "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared" ] }, "com.palantir.atlasdb:timestamp-api": { @@ -977,7 +978,8 @@ "project": true, "transitive": [ "com.palantir.atlasdb:atlasdb-api", - "com.palantir.atlasdb:atlasdb-client" + "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared" ] }, "com.palantir.atlasdb:timestamp-api": { diff --git a/qos-service-api/build.gradle b/qos-service-api/build.gradle index 60b3ef8dbf4..421a6862cd3 100644 --- a/qos-service-api/build.gradle +++ b/qos-service-api/build.gradle @@ -17,5 +17,8 @@ dependencies { exclude (module:'okhttp') exclude (module:'jsr305') } + + processor group: 'org.immutables', name: 'value' + } diff --git a/qos-service-api/src/main/java/com/palantir/atlasdb/qos/FakeQosClient.java b/qos-service-api/src/main/java/com/palantir/atlasdb/qos/FakeQosClient.java index 584b4422b1d..7ab0ab4a89d 100644 --- a/qos-service-api/src/main/java/com/palantir/atlasdb/qos/FakeQosClient.java +++ b/qos-service-api/src/main/java/com/palantir/atlasdb/qos/FakeQosClient.java @@ -18,14 +18,18 @@ public class FakeQosClient implements QosClient { - private static final FakeQosClient DEFAULT = new FakeQosClient(); + public static final FakeQosClient INSTANCE = new FakeQosClient(); - public static FakeQosClient getDefault() { - return DEFAULT; + @Override + public T executeRead(Query query, QueryWeigher weigher) + throws E { + return query.execute(); } @Override - public void checkLimit() { - // no op + public T executeWrite(Query query, QueryWeigher weigher) + throws E { + return query.execute(); } + } diff --git a/qos-service-api/src/main/java/com/palantir/atlasdb/qos/QosClient.java b/qos-service-api/src/main/java/com/palantir/atlasdb/qos/QosClient.java index c31757ad665..3c5be52a3bd 100644 --- a/qos-service-api/src/main/java/com/palantir/atlasdb/qos/QosClient.java +++ b/qos-service-api/src/main/java/com/palantir/atlasdb/qos/QosClient.java @@ -17,5 +17,23 @@ package com.palantir.atlasdb.qos; public interface QosClient { - void checkLimit(); + + interface Query { + T execute() throws E; + } + + interface QueryWeigher { + QueryWeight estimate(); + QueryWeight weighSuccess(T result, long timeTakenNanos); + QueryWeight weighFailure(Exception error, long timeTakenNanos); + } + + T executeRead( + Query query, + QueryWeigher weigher) throws E; + + T executeWrite( + Query query, + QueryWeigher weigher) throws E; + } diff --git a/qos-service-api/src/main/java/com/palantir/atlasdb/qos/QueryWeight.java b/qos-service-api/src/main/java/com/palantir/atlasdb/qos/QueryWeight.java new file mode 100644 index 00000000000..6e36a73c4dc --- /dev/null +++ b/qos-service-api/src/main/java/com/palantir/atlasdb/qos/QueryWeight.java @@ -0,0 +1,37 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the BSD-3 License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/BSD-3-Clause + * + * 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 com.palantir.atlasdb.qos; + +import java.util.concurrent.TimeUnit; + +import org.immutables.value.Value; + +@Value.Immutable +public interface QueryWeight { + + long numBytes(); + + long numDistinctRows(); + + // TODO(nziebart): need to standardize everyhting to longs, and handle casting to int in QosRateLimiter + long timeTakenNanos(); + + default long timeTakenMicros() { + return TimeUnit.NANOSECONDS.toMicros(timeTakenNanos()); + } + +} diff --git a/qos-service-api/src/main/java/com/palantir/atlasdb/qos/ratelimit/RateLimitExceededException.java b/qos-service-api/src/main/java/com/palantir/atlasdb/qos/ratelimit/RateLimitExceededException.java new file mode 100644 index 00000000000..50a7a58d520 --- /dev/null +++ b/qos-service-api/src/main/java/com/palantir/atlasdb/qos/ratelimit/RateLimitExceededException.java @@ -0,0 +1,31 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the BSD-3 License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/BSD-3-Clause + * + * 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 com.palantir.atlasdb.qos.ratelimit; + +/** + * This exception is thrown when a request is made through a {@link com.palantir.atlasdb.qos.QosClient}, but the + * request exceeds the limits defined by the QosClient in some way and thus cannot be completed. + */ +public class RateLimitExceededException extends RuntimeException { + public RateLimitExceededException(String msg) { + super(msg); + } + + public RateLimitExceededException(String msg, Throwable throwable) { + super(msg, throwable); + } +} diff --git a/qos-service-impl/build.gradle b/qos-service-impl/build.gradle index 014b66a60d8..75c1f1db3d6 100644 --- a/qos-service-impl/build.gradle +++ b/qos-service-impl/build.gradle @@ -15,8 +15,13 @@ jacocoTestReport { check.dependsOn integTest dependencies { + compile (project(":atlasdb-client")) { + exclude group: 'com.squareup.okhttp3' + exclude group: 'com.google.guava' + } compile (project(":qos-service-api")); + processor project(":atlasdb-processors") processor group: 'org.immutables', name: 'value' testCompile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml' diff --git a/qos-service-impl/src/integTest/java/com/palantir/atlasdb/qos/QosServiceIntegrationTest.java b/qos-service-impl/src/integTest/java/com/palantir/atlasdb/qos/QosServiceIntegrationTest.java index 51642b0c450..4d355fa2978 100644 --- a/qos-service-impl/src/integTest/java/com/palantir/atlasdb/qos/QosServiceIntegrationTest.java +++ b/qos-service-impl/src/integTest/java/com/palantir/atlasdb/qos/QosServiceIntegrationTest.java @@ -50,8 +50,8 @@ public class QosServiceIntegrationTest { @Test public void returnsConfiguredLimits() { - assertThat(service.getLimit("test")).isEqualTo(10L); - assertThat(service.getLimit("test2")).isEqualTo(20L); + assertThat(service.getLimit("test")).isEqualTo(10); + assertThat(service.getLimit("test2")).isEqualTo(20); } } diff --git a/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/client/AtlasDbQosClient.java b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/client/AtlasDbQosClient.java index 67c77542761..63007b163d6 100644 --- a/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/client/AtlasDbQosClient.java +++ b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/client/AtlasDbQosClient.java @@ -14,48 +14,81 @@ * limitations under the License. */ package com.palantir.atlasdb.qos.client; -import java.util.concurrent.ScheduledExecutorService; + +import java.time.Duration; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Stopwatch; +import com.google.common.base.Ticker; import com.palantir.atlasdb.qos.QosClient; -import com.palantir.atlasdb.qos.QosService; +import com.palantir.atlasdb.qos.QueryWeight; +import com.palantir.atlasdb.qos.metrics.QosMetrics; import com.palantir.atlasdb.qos.ratelimit.QosRateLimiter; +import com.palantir.atlasdb.qos.ratelimit.QosRateLimiters; +import com.palantir.atlasdb.qos.ratelimit.RateLimitExceededException; public class AtlasDbQosClient implements QosClient { - private final QosService qosService; - private final String clientName; - private final QosRateLimiter rateLimiter; - - private volatile long credits; - - public AtlasDbQosClient(QosService qosService, - ScheduledExecutorService limitRefresher, - String clientName, - QosRateLimiter rateLimiter) { - this.qosService = qosService; - this.clientName = clientName; - this.rateLimiter = rateLimiter; - limitRefresher.scheduleAtFixedRate(() -> { - try { - credits = qosService.getLimit(clientName); - } catch (Exception e) { - // do nothing - } - }, 0L, 60L, TimeUnit.SECONDS); + + private static final Logger log = LoggerFactory.getLogger(AtlasDbQosClient.class); + + private final QosRateLimiters rateLimiters; + private final QosMetrics metrics; + private final Ticker ticker; + + public static AtlasDbQosClient create(QosRateLimiters rateLimiters) { + return new AtlasDbQosClient(rateLimiters, new QosMetrics(), Ticker.systemTicker()); + } + + @VisibleForTesting + AtlasDbQosClient(QosRateLimiters rateLimiters, QosMetrics metrics, Ticker ticker) { + this.metrics = metrics; + this.rateLimiters = rateLimiters; + this.ticker = ticker; + } + + @Override + public T executeRead(Query query, QueryWeigher weigher) throws E { + return execute(query, weigher, rateLimiters.read(), metrics::recordRead); } - // The KVS layer should call this before every read/write operation - // Currently all operations are treated equally; each uses up a unit of credits @Override - public void checkLimit() { - // always return immediately - i.e. no backoff - // TODO if soft-limited, pause - // if hard-limited, throw exception - if (credits > 0) { - credits--; - } else { - // TODO This should be a ThrottleException? - throw new RuntimeException("Rate limit exceeded"); + public T executeWrite(Query query, QueryWeigher weigher) throws E { + return execute(query, weigher, rateLimiters.write(), metrics::recordWrite); + } + + private T execute( + Query query, + QueryWeigher weigher, + QosRateLimiter rateLimiter, + Consumer weightMetric) throws E { + long estimatedNumBytes = weigher.estimate().numBytes(); + try { + Duration waitTime = rateLimiter.consumeWithBackoff(estimatedNumBytes); + metrics.recordBackoffMicros(TimeUnit.NANOSECONDS.toMicros(waitTime.toNanos())); + } catch (RateLimitExceededException ex) { + metrics.recordRateLimitedException(); + throw ex; + } + + Stopwatch timer = Stopwatch.createStarted(ticker); + + QueryWeight actualWeight = null; + try { + T result = query.execute(); + actualWeight = weigher.weighSuccess(result, timer.elapsed(TimeUnit.NANOSECONDS)); + return result; + } catch (Exception ex) { + actualWeight = weigher.weighFailure(ex, timer.elapsed(TimeUnit.NANOSECONDS)); + throw ex; + } finally { + weightMetric.accept(actualWeight); + rateLimiter.recordAdjustment(actualWeight.numBytes() - estimatedNumBytes); } } + } diff --git a/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/config/QosClientConfig.java b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/config/QosClientConfig.java new file mode 100644 index 00000000000..313a02f3d7d --- /dev/null +++ b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/config/QosClientConfig.java @@ -0,0 +1,47 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the BSD-3 License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/BSD-3-Clause + * + * 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 com.palantir.atlasdb.qos.config; + +import java.util.Optional; + +import org.immutables.value.Value; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.palantir.remoting.api.config.service.HumanReadableDuration; +import com.palantir.remoting.api.config.service.ServiceConfiguration; + +@Value.Immutable +@JsonDeserialize(as = ImmutableQosClientConfig.class) +@JsonSerialize(as = ImmutableQosClientConfig.class) +public abstract class QosClientConfig { + + public static final QosClientConfig DEFAULT = ImmutableQosClientConfig.builder().build(); + + public abstract Optional qosService(); + + @Value.Default + public HumanReadableDuration maxBackoffSleepTime() { + return HumanReadableDuration.seconds(10); + } + + @Value.Default + public QosLimitsConfig limits() { + return QosLimitsConfig.DEFAULT_NO_LIMITS; + } + +} diff --git a/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/config/QosLimitsConfig.java b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/config/QosLimitsConfig.java new file mode 100644 index 00000000000..c1ed99ed922 --- /dev/null +++ b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/config/QosLimitsConfig.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the BSD-3 License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/BSD-3-Clause + * + * 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 com.palantir.atlasdb.qos.config; + +import org.immutables.value.Value; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +@Value.Immutable +@JsonDeserialize(as = ImmutableQosLimitsConfig.class) +@JsonSerialize(as = ImmutableQosLimitsConfig.class) +public abstract class QosLimitsConfig { + + public static final QosLimitsConfig DEFAULT_NO_LIMITS = ImmutableQosLimitsConfig.builder().build(); + + @Value.Default + public long readBytesPerSecond() { + return Long.MAX_VALUE; + } + + @Value.Default + public long writeBytesPerSecond() { + return Long.MAX_VALUE; + } + +} diff --git a/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/metrics/QosMetrics.java b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/metrics/QosMetrics.java new file mode 100644 index 00000000000..9d84764630e --- /dev/null +++ b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/metrics/QosMetrics.java @@ -0,0 +1,89 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the BSD-3 License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/BSD-3-Clause + * + * 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 com.palantir.atlasdb.qos.metrics; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.codahale.metrics.Meter; +import com.palantir.atlasdb.qos.QueryWeight; +import com.palantir.atlasdb.util.MetricsManager; +import com.palantir.logsafe.SafeArg; + +// TODO(nziebart): needs tests +public class QosMetrics { + + private static final Logger log = LoggerFactory.getLogger(QosMetrics.class); + + private final MetricsManager metricsManager = new MetricsManager(); + + private final Meter readRequestCount; + private final Meter bytesRead; + private final Meter readTime; + private final Meter rowsRead; + + private final Meter writeRequestCount; + private final Meter bytesWritten; + private final Meter writeTime; + private final Meter rowsWritten; + + private final Meter backoffTime; + private final Meter rateLimitedExceptions; + + public QosMetrics() { + readRequestCount = metricsManager.registerMeter(QosMetrics.class, "numReadRequests"); + bytesRead = metricsManager.registerMeter(QosMetrics.class, "bytesRead"); + readTime = metricsManager.registerMeter(QosMetrics.class, "readTime"); + rowsRead = metricsManager.registerMeter(QosMetrics.class, "rowsRead"); + + writeRequestCount = metricsManager.registerMeter(QosMetrics.class, "numWriteRequests"); + bytesWritten = metricsManager.registerMeter(QosMetrics.class, "bytesWritten"); + writeTime = metricsManager.registerMeter(QosMetrics.class, "writeTime"); + rowsWritten = metricsManager.registerMeter(QosMetrics.class, "rowsWritten"); + + backoffTime = metricsManager.registerMeter(QosMetrics.class, "backoffTime"); + rateLimitedExceptions = metricsManager.registerMeter(QosMetrics.class, "rateLimitedExceptions"); + } + + public void recordRead(QueryWeight weight) { + readRequestCount.mark(); + bytesRead.mark(weight.numBytes()); + readTime.mark(weight.timeTakenMicros()); + rowsRead.mark(weight.numDistinctRows()); + } + + public void recordWrite(QueryWeight weight) { + writeRequestCount.mark(); + bytesWritten.mark(weight.numBytes()); + writeTime.mark(weight.timeTakenMicros()); + rowsWritten.mark(weight.numDistinctRows()); + } + + public void recordBackoffMicros(long backoffTimeMicros) { + if (backoffTimeMicros > 0) { + log.info("Backing off for {} micros", SafeArg.of("backoffTimeMicros", backoffTimeMicros)); + backoffTime.mark(backoffTimeMicros); + } + } + + public void recordRateLimitedException() { + log.info("Rate limit exceeded and backoff time would be more than the configured maximum. " + + "Throwing a throttling exception"); + rateLimitedExceptions.mark(); + } + +} diff --git a/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/QosRateLimiter.java b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/QosRateLimiter.java index 4112e03d9e1..d4d47eeaa0a 100644 --- a/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/QosRateLimiter.java +++ b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/QosRateLimiter.java @@ -19,8 +19,16 @@ import java.time.Duration; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; +import com.google.common.primitives.Ints; +import com.palantir.atlasdb.qos.ratelimit.guava.RateLimiter; +import com.palantir.atlasdb.qos.ratelimit.guava.SmoothRateLimiter; +import com.palantir.logsafe.SafeArg; /** * A rate limiter for database queries, based on "units" of expense. This limiter strives to maintain an upper limit on @@ -32,30 +40,30 @@ */ public class QosRateLimiter { - private static final double MAX_BURST_SECONDS = 5; - private static final double UNLIMITED_RATE = Double.MAX_VALUE; - private static final int MAX_WAIT_TIME_SECONDS = 10; + private static final Logger log = LoggerFactory.getLogger(QosRateLimiter.class); - private RateLimiter rateLimiter; + private static final long MAX_BURST_SECONDS = 5; - public static QosRateLimiter create() { - return new QosRateLimiter(RateLimiter.SleepingStopwatch.createFromSystemTimer()); - } + private final Supplier maxBackoffTimeMillis; + private final Supplier unitsPerSecond; + private final RateLimiter.SleepingStopwatch stopwatch; - @VisibleForTesting - QosRateLimiter(RateLimiter.SleepingStopwatch stopwatch) { - rateLimiter = new SmoothRateLimiter.SmoothBursty( - stopwatch, - MAX_BURST_SECONDS); + private volatile RateLimiter rateLimiter; + private volatile long currentRate; - rateLimiter.setRate(UNLIMITED_RATE); + public static QosRateLimiter create(Supplier maxBackoffTimeMillis, Supplier unitsPerSecond) { + return new QosRateLimiter(RateLimiter.SleepingStopwatch.createFromSystemTimer(), maxBackoffTimeMillis, + unitsPerSecond); } - /** - * Update the allowed rate, in units per second. - */ - public void updateRate(int unitsPerSecond) { - rateLimiter.setRate(unitsPerSecond); + @VisibleForTesting + QosRateLimiter(RateLimiter.SleepingStopwatch stopwatch, Supplier maxBackoffTimeMillis, + Supplier unitsPerSecond) { + this.stopwatch = stopwatch; + this.unitsPerSecond = unitsPerSecond; + this.maxBackoffTimeMillis = maxBackoffTimeMillis; + + createRateLimiterAtomically(); } /** @@ -64,27 +72,54 @@ public void updateRate(int unitsPerSecond) { * * @return the amount of time slept for, if any */ - public Duration consumeWithBackoff(int estimatedNumUnits) { + public Duration consumeWithBackoff(long estimatedNumUnits) { + updateRateIfNeeded(); + Optional waitTime = rateLimiter.tryAcquire( - estimatedNumUnits, - MAX_WAIT_TIME_SECONDS, - TimeUnit.SECONDS); + Ints.saturatedCast(estimatedNumUnits), // TODO(nziebart): deal with longs + maxBackoffTimeMillis.get(), + TimeUnit.MILLISECONDS); if (!waitTime.isPresent()) { - throw new RuntimeException("rate limited"); + throw new RateLimitExceededException("Rate limited. Available capacity has been exhausted."); } return waitTime.get(); } /** - * Records an adjustment to the original estimate of units consumed passed to {@link #consumeWithBackoff(int)}. This + * The RateLimiter's rate requires a lock acquisition to read, and is returned as a double. To avoid + * overhead and double comparisons, we maintain the current rate ourselves. + */ + private void updateRateIfNeeded() { + if (currentRate != unitsPerSecond.get()) { + createRateLimiterAtomically(); + } + } + + /** + * Guava's RateLimiter has strange behavior around updating the rate. Namely, if you set the rate very small and ask + * for a large number of permits, you will end up having to wait until that small rate is satisfied before acquiring + * more, even if you update the rate to something very large. So, we just create a new rate limiter if the rate + * changes. + */ + private synchronized void createRateLimiterAtomically() { + currentRate = unitsPerSecond.get(); + rateLimiter = new SmoothRateLimiter.SmoothBursty(stopwatch, MAX_BURST_SECONDS); + rateLimiter.setRate(currentRate); + + // TODO(nziebart): distinguish between read/write rate limiters + log.info("Units per second set to {}", SafeArg.of("unitsPerSecond", currentRate)); + } + + /** + * Records an adjustment to the original estimate of units consumed passed to {@link #consumeWithBackoff}. This * should be called after a query returns, when the exact number of units consumed is known. This value may be - * positive (if the original estimate was too small) or negative (if the original estimate was too large. + * positive (if the original estimate was too small) or negative (if the original estimate was too large). */ - public void recordAdjustment(int adjustmentUnits) { + public void recordAdjustment(long adjustmentUnits) { if (adjustmentUnits > 0) { - rateLimiter.steal(adjustmentUnits); + rateLimiter.steal(Ints.saturatedCast(adjustmentUnits)); // TODO(nziebart): deal with longs } // TODO(nziebart): handle negative case } diff --git a/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/QosRateLimiters.java b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/QosRateLimiters.java new file mode 100644 index 00000000000..8b7d8753dd2 --- /dev/null +++ b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/QosRateLimiters.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the BSD-3 License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/BSD-3-Clause + * + * 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 com.palantir.atlasdb.qos.ratelimit; + +import java.util.function.Supplier; + +import org.immutables.value.Value; + +import com.palantir.atlasdb.qos.config.QosLimitsConfig; + +@Value.Immutable +public interface QosRateLimiters { + + static QosRateLimiters create(Supplier maxBackoffSleepTimeMillis, Supplier config) { + QosRateLimiter readLimiter = QosRateLimiter.create(maxBackoffSleepTimeMillis, + () -> config.get().readBytesPerSecond()); + + QosRateLimiter writeLimiter = QosRateLimiter.create(maxBackoffSleepTimeMillis, + () -> config.get().writeBytesPerSecond()); + + return ImmutableQosRateLimiters.builder() + .read(readLimiter) + .write(writeLimiter) + .build(); + } + + QosRateLimiter read(); + + QosRateLimiter write(); + +} diff --git a/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/RateLimiter.java b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/RateLimiter.java deleted file mode 100644 index b53460dd180..00000000000 --- a/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/RateLimiter.java +++ /dev/null @@ -1,306 +0,0 @@ -//CHECKSTYLE:OFF -/* - * Copyright (C) 2012 The Guava 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 com.palantir.atlasdb.qos.ratelimit; - -import static java.lang.Math.max; -import static java.util.concurrent.TimeUnit.MICROSECONDS; -import static java.util.concurrent.TimeUnit.SECONDS; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - -import java.time.Duration; -import java.util.Locale; -import java.util.Optional; -import java.util.concurrent.TimeUnit; - -import com.google.common.base.Stopwatch; -import com.google.common.util.concurrent.Uninterruptibles; - -/** - * Copied from Guava, because {@link com.palantir.atlasdb.qos.ratelimit.SmoothRateLimiter.SmoothBursty} is a package - * private class. - * - * There are also a few minor but notable modifications: - * 1) {@link #tryAcquire(int, long, TimeUnit)} returns an optional duration rather than a boolean. This is analogous - * to the return value of {@link #acquire()}. - * 2) A new method {@link #steal(int)} was added, to support taking permits without waiting - * 3) Some static constructors were removed. - **/ -public abstract class RateLimiter { - - /** - * The underlying timer; used both to measure elapsed time and sleep as necessary. A separate - * object to facilitate testing. - */ - private final SleepingStopwatch stopwatch; - - // Can't be initialized in the constructor because mocks don't call the constructor. - private volatile Object mutexDoNotUseDirectly; - - private Object mutex() { - Object mutex = mutexDoNotUseDirectly; - if (mutex == null) { - synchronized (this) { - mutex = mutexDoNotUseDirectly; - if (mutex == null) { - mutexDoNotUseDirectly = mutex = new Object(); - } - } - } - return mutex; - } - - RateLimiter(SleepingStopwatch stopwatch) { - this.stopwatch = checkNotNull(stopwatch); - } - - /** - * Updates the stable rate of this {@code RateLimiter}, that is, the {@code permitsPerSecond} - * argument provided in the factory method that constructed the {@code RateLimiter}. Currently - * throttled threads will not be awakened as a result of this invocation, thus they do not - * observe the new rate; only subsequent requests will. - * - *

Note though that, since each request repays (by waiting, if necessary) the cost of the - * previous request, this means that the very next request after an invocation to - * {@code setRate} will not be affected by the new rate; it will pay the cost of the previous - * request, which is in terms of the previous rate. - * - *

The behavior of the {@code RateLimiter} is not modified in any other way, e.g. if the - * {@code RateLimiter} was configured with a warmup period of 20 seconds, it still has a warmup - * period of 20 seconds after this method invocation. - * - * @param permitsPerSecond the new stable rate of this {@code RateLimiter} - * @throws IllegalArgumentException if {@code permitsPerSecond} is negative or zero - */ - public final void setRate(double permitsPerSecond) { - checkArgument( - permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive"); - synchronized (mutex()) { - doSetRate(permitsPerSecond, stopwatch.readMicros()); - } - } - - abstract void doSetRate(double permitsPerSecond, long nowMicros); - - /** - * Returns the stable rate (as {@code permits per seconds}) with which this {@code RateLimiter} is - * configured with. The initial value of this is the same as the {@code permitsPerSecond} argument - * passed in the factory method that produced this {@code RateLimiter}, and it is only updated - * after invocations to {@linkplain #setRate}. - */ - public final double getRate() { - synchronized (mutex()) { - return doGetRate(); - } - } - - abstract double doGetRate(); - - /** - * Acquires a single permit from this {@code RateLimiter}, blocking until the request can be - * granted. Tells the amount of time slept, if any. - * - *

This method is equivalent to {@code acquire(1)}. - * - * @return time spent sleeping to enforce rate, in seconds; 0.0 if not rate-limited - * @since 16.0 (present in 13.0 with {@code void} return type}) - */ - public double acquire() { - return acquire(1); - } - - /** - * Acquires the given number of permits from this {@code RateLimiter}, blocking until the request - * can be granted. Tells the amount of time slept, if any. - * - * @param permits the number of permits to acquire - * @return time spent sleeping to enforce rate, in seconds; 0.0 if not rate-limited - * @throws IllegalArgumentException if the requested number of permits is negative or zero - * @since 16.0 (present in 13.0 with {@code void} return type}) - */ - public double acquire(int permits) { - long microsToWait = reserve(permits); - stopwatch.sleepMicrosUninterruptibly(microsToWait); - return 1.0 * microsToWait / SECONDS.toMicros(1L); - } - - /** - * Immediately steals the given number of permits. This will potentially penalize future callers, but has no - * effect on callers that are already waiting for permits. - */ - public void steal(int permits) { - reserve(permits); - } - - /** - * Reserves the given number of permits from this {@code RateLimiter} for future use, returning - * the number of microseconds until the reservation can be consumed. - * - * @return time in microseconds to wait until the resource can be acquired, never negative - */ - final long reserve(int permits) { - checkPermits(permits); - synchronized (mutex()) { - return reserveAndGetWaitLength(permits, stopwatch.readMicros()); - } - } - - /** - * Acquires a permit from this {@code RateLimiter} if it can be obtained without exceeding the - * specified {@code timeout}, or returns {@code false} immediately (without waiting) if the permit - * would not have been granted before the timeout expired. - * - *

This method is equivalent to {@code tryAcquire(1, timeout, unit)}. - * - * @param timeout the maximum time to wait for the permit. Negative values are treated as zero. - * @param unit the time unit of the timeout argument - * @return {@code true} if the permit was acquired, {@code false} otherwise - * @throws IllegalArgumentException if the requested number of permits is negative or zero - */ - public boolean tryAcquire(long timeout, TimeUnit unit) { - return tryAcquire(1, timeout, unit).isPresent(); - } - - /** - * Acquires permits from this {@link com.google.common.util.concurrent.RateLimiter} if it can be acquired immediately without delay. - * - *

This method is equivalent to {@code tryAcquire(permits, 0, anyUnit)}. - * - * @param permits the number of permits to acquire - * @return {@code true} if the permits were acquired, {@code false} otherwise - * @throws IllegalArgumentException if the requested number of permits is negative or zero - * @since 14.0 - */ - public boolean tryAcquire(int permits) { - return tryAcquire(permits, 0, MICROSECONDS).isPresent(); - } - - /** - * Acquires a permit from this {@link com.google.common.util.concurrent.RateLimiter} if it can be acquired immediately without - * delay. - * - *

This method is equivalent to {@code tryAcquire(1)}. - * - * @return {@code true} if the permit was acquired, {@code false} otherwise - * @since 14.0 - */ - public boolean tryAcquire() { - return tryAcquire(1, 0, MICROSECONDS).isPresent(); - } - - /** - * Acquires the given number of permits from this {@code RateLimiter} if it can be obtained - * without exceeding the specified {@code timeout}, or returns {@code false} immediately (without - * waiting) if the permits would not have been granted before the timeout expired. - * - * @param permits the number of permits to acquire - * @param timeout the maximum time to wait for the permits. Negative values are treated as zero. - * @param unit the time unit of the timeout argument - * @return amount of time waited, if the permits were acquired, empty otherwise - * @throws IllegalArgumentException if the requested number of permits is negative or zero - */ - public Optional tryAcquire(int permits, long timeout, TimeUnit unit) { - long timeoutMicros = max(unit.toMicros(timeout), 0); - checkPermits(permits); - long microsToWait; - synchronized (mutex()) { - long nowMicros = stopwatch.readMicros(); - if (!canAcquire(nowMicros, timeoutMicros)) { - return Optional.empty(); - } else { - microsToWait = reserveAndGetWaitLength(permits, nowMicros); - } - } - stopwatch.sleepMicrosUninterruptibly(microsToWait); - return Optional.of(Duration.ofNanos(TimeUnit.MICROSECONDS.toNanos(microsToWait))); - } - - private boolean canAcquire(long nowMicros, long timeoutMicros) { - return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros; - } - - /** - * Reserves next ticket and returns the wait time that the caller must wait for. - * - * @return the required wait time, never negative - */ - final long reserveAndGetWaitLength(int permits, long nowMicros) { - long momentAvailable = reserveEarliestAvailable(permits, nowMicros); - return max(momentAvailable - nowMicros, 0); - } - - /** - * Returns the earliest time that permits are available (with one caveat). - * - * @return the time that permits are available, or, if permits are available immediately, an - * arbitrary past or present time - */ - abstract long queryEarliestAvailable(long nowMicros); - - /** - * Reserves the requested number of permits and returns the time that those permits can be used - * (with one caveat). - * - * @return the time that the permits may be used, or, if the permits may be used immediately, an - * arbitrary past or present time - */ - abstract long reserveEarliestAvailable(int permits, long nowMicros); - - @Override - public String toString() { - return String.format(Locale.ROOT, "RateLimiter[stableRate=%3.1fqps]", getRate()); - } - - abstract static class SleepingStopwatch { - /** Constructor for use by subclasses. */ - protected SleepingStopwatch() {} - - /* - * We always hold the mutex when calling this. TODO(cpovirk): Is that important? Perhaps we need - * to guarantee that each call to reserveEarliestAvailable, etc. sees a value >= the previous? - * Also, is it OK that we don't hold the mutex when sleeping? - */ - protected abstract long readMicros(); - - protected abstract void sleepMicrosUninterruptibly(long micros); - - public static final SleepingStopwatch createFromSystemTimer() { - return new SleepingStopwatch() { - final Stopwatch stopwatch = Stopwatch.createStarted(); - - @Override - protected long readMicros() { - return stopwatch.elapsed(MICROSECONDS); - } - - @Override - protected void sleepMicrosUninterruptibly(long micros) { - if (micros > 0) { - Uninterruptibles.sleepUninterruptibly(micros, MICROSECONDS); - } - } - }; - } - } - - private static void checkPermits(int permits) { - checkArgument(permits > 0, "Requested permits (%s) must be positive", permits); - } -} - diff --git a/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/SmoothRateLimiter.java b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/SmoothRateLimiter.java deleted file mode 100644 index 465b0141152..00000000000 --- a/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/SmoothRateLimiter.java +++ /dev/null @@ -1,156 +0,0 @@ -// CHECKSTYLE:OFF -/* - * Copyright (C) 2012 The Guava 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 com.palantir.atlasdb.qos.ratelimit; - -import static java.lang.Math.min; -import static java.util.concurrent.TimeUnit.SECONDS; - -import com.google.common.math.LongMath; - -/** - * Copied from Guava, because {@link SmoothBursty} is a package private class. - **/ -abstract class SmoothRateLimiter extends RateLimiter { - /** - * This implements a "bursty" RateLimiter, where storedPermits are translated to zero throttling. - * The maximum number of permits that can be saved (when the RateLimiter is unused) is defined in - * terms of time, in this sense: if a RateLimiter is 2qps, and this time is specified as 10 - * seconds, we can save up to 2 * 10 = 20 permits. - */ - static final class SmoothBursty extends SmoothRateLimiter { - /** The work (permits) of how many seconds can be saved up if this RateLimiter is unused? */ - final double maxBurstSeconds; - - SmoothBursty(SleepingStopwatch stopwatch, double maxBurstSeconds) { - super(stopwatch); - this.maxBurstSeconds = maxBurstSeconds; - } - - @Override - void doSetRate(double permitsPerSecond, double stableIntervalMicros) { - double oldMaxPermits = this.maxPermits; - maxPermits = maxBurstSeconds * permitsPerSecond; - if (oldMaxPermits == Double.POSITIVE_INFINITY) { - // if we don't special-case this, we would get storedPermits == NaN, below - storedPermits = maxPermits; - } else { - storedPermits = - (oldMaxPermits == 0.0) - ? 0.0 // initial state - : storedPermits * maxPermits / oldMaxPermits; - } - } - - @Override - long storedPermitsToWaitTime(double storedPermits, double permitsToTake) { - return 0L; - } - - @Override - double coolDownIntervalMicros() { - return stableIntervalMicros; - } - } - - /** - * The currently stored permits. - */ - double storedPermits; - - /** - * The maximum number of stored permits. - */ - double maxPermits; - - /** - * The interval between two unit requests, at our stable rate. E.g., a stable rate of 5 permits - * per second has a stable interval of 200ms. - */ - double stableIntervalMicros; - - /** - * The time when the next request (no matter its size) will be granted. After granting a request, - * this is pushed further in the future. Large requests push this further than small requests. - */ - private long nextFreeTicketMicros = 0L; // could be either in the past or future - - private SmoothRateLimiter(SleepingStopwatch stopwatch) { - super(stopwatch); - } - - @Override - final void doSetRate(double permitsPerSecond, long nowMicros) { - resync(nowMicros); - double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond; - this.stableIntervalMicros = stableIntervalMicros; - doSetRate(permitsPerSecond, stableIntervalMicros); - } - - abstract void doSetRate(double permitsPerSecond, double stableIntervalMicros); - - @Override - final double doGetRate() { - return SECONDS.toMicros(1L) / stableIntervalMicros; - } - - @Override - final long queryEarliestAvailable(long nowMicros) { - return nextFreeTicketMicros; - } - - @Override - final long reserveEarliestAvailable(int requiredPermits, long nowMicros) { - resync(nowMicros); - long returnValue = nextFreeTicketMicros; - double storedPermitsToSpend = min(requiredPermits, this.storedPermits); - double freshPermits = requiredPermits - storedPermitsToSpend; - long waitMicros = - storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend) - + (long) (freshPermits * stableIntervalMicros); - - this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros); - this.storedPermits -= storedPermitsToSpend; - return returnValue; - } - - /** - * Translates a specified portion of our currently stored permits which we want to spend/acquire, - * into a throttling time. Conceptually, this evaluates the integral of the underlying function we - * use, for the range of [(storedPermits - permitsToTake), storedPermits]. - * - *

This always holds: {@code 0 <= permitsToTake <= storedPermits} - */ - abstract long storedPermitsToWaitTime(double storedPermits, double permitsToTake); - - /** - * Returns the number of microseconds during cool down that we have to wait to get a new permit. - */ - abstract double coolDownIntervalMicros(); - - /** - * Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time. - */ - void resync(long nowMicros) { - // if nextFreeTicket is in the past, resync to now - if (nowMicros > nextFreeTicketMicros) { - double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros(); - storedPermits = min(maxPermits, storedPermits + newPermits); - nextFreeTicketMicros = nowMicros; - } - } -} diff --git a/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/guava/LICENSE b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/guava/LICENSE new file mode 100644 index 00000000000..d6aac6c3516 --- /dev/null +++ b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/guava/LICENSE @@ -0,0 +1,53 @@ +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and +You must cause any modified files to carry prominent notices stating that You changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/guava/RateLimiter.java b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/guava/RateLimiter.java new file mode 100644 index 00000000000..59ff887ab81 --- /dev/null +++ b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/guava/RateLimiter.java @@ -0,0 +1,464 @@ +//CHECKSTYLE:OFF +/* + * Copyright (C) 2012 The Guava 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. + */ + +// CHANGELOG: package name changed +package com.palantir.atlasdb.qos.ratelimit.guava; + +import static java.lang.Math.max; +import static java.util.concurrent.TimeUnit.MICROSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.time.Duration; +import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import javax.annotation.concurrent.ThreadSafe; + +import com.google.common.annotations.Beta; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Stopwatch; +import com.google.common.util.concurrent.Uninterruptibles; +import com.palantir.atlasdb.qos.ratelimit.guava.SmoothRateLimiter.SmoothBursty; +import com.palantir.atlasdb.qos.ratelimit.guava.SmoothRateLimiter.SmoothWarmingUp; + +/** + * A rate limiter. Conceptually, a rate limiter distributes permits at a + * configurable rate. Each {@link #acquire()} blocks if necessary until a permit is + * available, and then takes it. Once acquired, permits need not be released. + * + *

Rate limiters are often used to restrict the rate at which some + * physical or logical resource is accessed. This is in contrast to {@link + * java.util.concurrent.Semaphore} which restricts the number of concurrent + * accesses instead of the rate (note though that concurrency and rate are closely related, + * e.g. see Little's Law). + * + *

A {@code RateLimiter} is defined primarily by the rate at which permits + * are issued. Absent additional configuration, permits will be distributed at a + * fixed rate, defined in terms of permits per second. Permits will be distributed + * smoothly, with the delay between individual permits being adjusted to ensure + * that the configured rate is maintained. + * + *

It is possible to configure a {@code RateLimiter} to have a warmup + * period during which time the permits issued each second steadily increases until + * it hits the stable rate. + * + *

As an example, imagine that we have a list of tasks to execute, but we don't want to + * submit more than 2 per second: + *

  {@code
+ *  final RateLimiter rateLimiter = RateLimiter.create(2.0); // rate is "2 permits per second"
+ *  void submitTasks(List tasks, Executor executor) {
+ *    for (Runnable task : tasks) {
+ *      rateLimiter.acquire(); // may wait
+ *      executor.execute(task);
+ *    }
+ *  }
+ *}
+ * + *

As another example, imagine that we produce a stream of data, and we want to cap it + * at 5kb per second. This could be accomplished by requiring a permit per byte, and specifying + * a rate of 5000 permits per second: + *

  {@code
+ *  final RateLimiter rateLimiter = RateLimiter.create(5000.0); // rate = 5000 permits per second
+ *  void submitPacket(byte[] packet) {
+ *    rateLimiter.acquire(packet.length);
+ *    networkService.send(packet);
+ *  }
+ *}
+ * + *

It is important to note that the number of permits requested never + * affects the throttling of the request itself (an invocation to {@code acquire(1)} + * and an invocation to {@code acquire(1000)} will result in exactly the same throttling, if any), + * but it affects the throttling of the next request. I.e., if an expensive task + * arrives at an idle RateLimiter, it will be granted immediately, but it is the next + * request that will experience extra throttling, thus paying for the cost of the expensive + * task. + * + *

Note: {@code RateLimiter} does not provide fairness guarantees. + * + * @author Dimitris Andreou + * @since 13.0 + */ +// TODO(user): switch to nano precision. A natural unit of cost is "bytes", and a micro precision +// would mean a maximum rate of "1MB/s", which might be small in some cases. +@ThreadSafe +@Beta +public abstract class RateLimiter { + /** + * Creates a {@code RateLimiter} with the specified stable throughput, given as + * "permits per second" (commonly referred to as QPS, queries per second). + * + *

The returned {@code RateLimiter} ensures that on average no more than {@code + * permitsPerSecond} are issued during any given second, with sustained requests + * being smoothly spread over each second. When the incoming request rate exceeds + * {@code permitsPerSecond} the rate limiter will release one permit every {@code + * (1.0 / permitsPerSecond)} seconds. When the rate limiter is unused, + * bursts of up to {@code permitsPerSecond} permits will be allowed, with subsequent + * requests being smoothly limited at the stable rate of {@code permitsPerSecond}. + * + * @param permitsPerSecond the rate of the returned {@code RateLimiter}, measured in + * how many permits become available per second + * @throws IllegalArgumentException if {@code permitsPerSecond} is negative or zero + */ + // TODO(user): "This is equivalent to + // {@code createWithCapacity(permitsPerSecond, 1, TimeUnit.SECONDS)}". + public static RateLimiter create(double permitsPerSecond) { + /* + * The default RateLimiter configuration can save the unused permits of up to one second. + * This is to avoid unnecessary stalls in situations like this: A RateLimiter of 1qps, + * and 4 threads, all calling acquire() at these moments: + * + * T0 at 0 seconds + * T1 at 1.05 seconds + * T2 at 2 seconds + * T3 at 3 seconds + * + * Due to the slight delay of T1, T2 would have to sleep till 2.05 seconds, + * and T3 would also have to sleep till 3.05 seconds. + */ + return create(SleepingStopwatch.createFromSystemTimer(), permitsPerSecond); + } + + /* + * TODO(cpovirk): make SleepingStopwatch the last parameter throughout the class so that the + * overloads follow the usual convention: Foo(int), Foo(int, SleepingStopwatch) + */ + @VisibleForTesting + static RateLimiter create(SleepingStopwatch stopwatch, double permitsPerSecond) { + RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */); + rateLimiter.setRate(permitsPerSecond); + return rateLimiter; + } + + /** + * Creates a {@code RateLimiter} with the specified stable throughput, given as + * "permits per second" (commonly referred to as QPS, queries per second), and a + * warmup period, during which the {@code RateLimiter} smoothly ramps up its rate, + * until it reaches its maximum rate at the end of the period (as long as there are enough + * requests to saturate it). Similarly, if the {@code RateLimiter} is left unused for + * a duration of {@code warmupPeriod}, it will gradually return to its "cold" state, + * i.e. it will go through the same warming up process as when it was first created. + * + *

The returned {@code RateLimiter} is intended for cases where the resource that actually + * fulfills the requests (e.g., a remote server) needs "warmup" time, rather than + * being immediately accessed at the stable (maximum) rate. + * + *

The returned {@code RateLimiter} starts in a "cold" state (i.e. the warmup period + * will follow), and if it is left unused for long enough, it will return to that state. + * + * @param permitsPerSecond the rate of the returned {@code RateLimiter}, measured in + * how many permits become available per second + * @param warmupPeriod the duration of the period where the {@code RateLimiter} ramps up its + * rate, before reaching its stable (maximum) rate + * @param unit the time unit of the warmupPeriod argument + * @throws IllegalArgumentException if {@code permitsPerSecond} is negative or zero or + * {@code warmupPeriod} is negative + */ + public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) { + checkArgument(warmupPeriod >= 0, "warmupPeriod must not be negative: %s", warmupPeriod); + return create(SleepingStopwatch.createFromSystemTimer(), permitsPerSecond, warmupPeriod, unit, + 3.0); + } + + @VisibleForTesting + static RateLimiter create( + SleepingStopwatch stopwatch, double permitsPerSecond, long warmupPeriod, TimeUnit unit, + double coldFactor) { + RateLimiter rateLimiter = new SmoothWarmingUp(stopwatch, warmupPeriod, unit, coldFactor); + rateLimiter.setRate(permitsPerSecond); + return rateLimiter; + } + + /** + * The underlying timer; used both to measure elapsed time and sleep as necessary. A separate + * object to facilitate testing. + */ + private final SleepingStopwatch stopwatch; + + // Can't be initialized in the constructor because mocks don't call the constructor. + private volatile Object mutexDoNotUseDirectly; + + private Object mutex() { + Object mutex = mutexDoNotUseDirectly; + if (mutex == null) { + synchronized (this) { + mutex = mutexDoNotUseDirectly; + if (mutex == null) { + mutexDoNotUseDirectly = mutex = new Object(); + } + } + } + return mutex; + } + + RateLimiter(SleepingStopwatch stopwatch) { + this.stopwatch = checkNotNull(stopwatch); + } + + /** + * Updates the stable rate of this {@code RateLimiter}, that is, the + * {@code permitsPerSecond} argument provided in the factory method that + * constructed the {@code RateLimiter}. Currently throttled threads will not + * be awakened as a result of this invocation, thus they do not observe the new rate; + * only subsequent requests will. + * + *

Note though that, since each request repays (by waiting, if necessary) the cost + * of the previous request, this means that the very next request + * after an invocation to {@code setRate} will not be affected by the new rate; + * it will pay the cost of the previous request, which is in terms of the previous rate. + * + *

The behavior of the {@code RateLimiter} is not modified in any other way, + * e.g. if the {@code RateLimiter} was configured with a warmup period of 20 seconds, + * it still has a warmup period of 20 seconds after this method invocation. + * + * @param permitsPerSecond the new stable rate of this {@code RateLimiter} + * @throws IllegalArgumentException if {@code permitsPerSecond} is negative or zero + */ + public final void setRate(double permitsPerSecond) { + checkArgument( + permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive"); + synchronized (mutex()) { + doSetRate(permitsPerSecond, stopwatch.readMicros()); + } + } + + abstract void doSetRate(double permitsPerSecond, long nowMicros); + + /** + * Returns the stable rate (as {@code permits per seconds}) with which this + * {@code RateLimiter} is configured with. The initial value of this is the same as + * the {@code permitsPerSecond} argument passed in the factory method that produced + * this {@code RateLimiter}, and it is only updated after invocations + * to {@linkplain #setRate}. + */ + public final double getRate() { + synchronized (mutex()) { + return doGetRate(); + } + } + + abstract double doGetRate(); + + /** + * Acquires a single permit from this {@code RateLimiter}, blocking until the + * request can be granted. Tells the amount of time slept, if any. + * + *

This method is equivalent to {@code acquire(1)}. + * + * @return time spent sleeping to enforce rate, in seconds; 0.0 if not rate-limited + * @since 16.0 (present in 13.0 with {@code void} return type}) + */ + public double acquire() { + return acquire(1); + } + + /** + * Acquires the given number of permits from this {@code RateLimiter}, blocking until the + * request can be granted. Tells the amount of time slept, if any. + * + * @param permits the number of permits to acquire + * @return time spent sleeping to enforce rate, in seconds; 0.0 if not rate-limited + * @throws IllegalArgumentException if the requested number of permits is negative or zero + * @since 16.0 (present in 13.0 with {@code void} return type}) + */ + public double acquire(int permits) { + long microsToWait = reserve(permits); + stopwatch.sleepMicrosUninterruptibly(microsToWait); + return 1.0 * microsToWait / SECONDS.toMicros(1L); + } + + /** + * Reserves the given number of permits from this {@code RateLimiter} for future use, returning + * the number of microseconds until the reservation can be consumed. + * + * @return time in microseconds to wait until the resource can be acquired, never negative + */ + final long reserve(int permits) { + checkPermits(permits); + synchronized (mutex()) { + return reserveAndGetWaitLength(permits, stopwatch.readMicros()); + } + } + + /** + * Acquires a permit from this {@code RateLimiter} if it can be obtained + * without exceeding the specified {@code timeout}, or returns {@code false} + * immediately (without waiting) if the permit would not have been granted + * before the timeout expired. + * + *

This method is equivalent to {@code tryAcquire(1, timeout, unit)}. + * + * @param timeout the maximum time to wait for the permit. Negative values are treated as zero. + * @param unit the time unit of the timeout argument + * @return {@code true} if the permit was acquired, {@code false} otherwise + * @throws IllegalArgumentException if the requested number of permits is negative or zero + */ + public boolean tryAcquire(long timeout, TimeUnit unit) { + // CHANGELOG: boolean returned value now inferred from Optional presence + return tryAcquire(1, timeout, unit).isPresent(); + } + + /** + * Acquires permits from this {@link RateLimiter} if it can be acquired immediately without delay. + * + *

+ * This method is equivalent to {@code tryAcquire(permits, 0, anyUnit)}. + * + * @param permits the number of permits to acquire + * @return {@code true} if the permits were acquired, {@code false} otherwise + * @throws IllegalArgumentException if the requested number of permits is negative or zero + * @since 14.0 + */ + public boolean tryAcquire(int permits) { + // CHANGELOG: boolean returned value now inferred from Optional presence + return tryAcquire(permits, 0, MICROSECONDS).isPresent(); + } + + /** + * Acquires a permit from this {@link RateLimiter} if it can be acquired immediately without + * delay. + * + *

+ * This method is equivalent to {@code tryAcquire(1)}. + * + * @return {@code true} if the permit was acquired, {@code false} otherwise + * @since 14.0 + */ + public boolean tryAcquire() { + // CHANGELOG: boolean returned value now inferred from Optional presence + return tryAcquire(1, 0, MICROSECONDS).isPresent(); + } + + /** + * Acquires the given number of permits from this {@code RateLimiter} if it can be obtained + * without exceeding the specified {@code timeout}, or returns {@code false} + * immediately (without waiting) if the permits would not have been granted + * before the timeout expired. + * + * @param permits the number of permits to acquire + * @param timeout the maximum time to wait for the permits. Negative values are treated as zero. + * @param unit the time unit of the timeout argument + * // CHANGELOG: docs changed to reflect different return value + * @return amount of time waited, if the permits were acquired, empty otherwise + * @throws IllegalArgumentException if the requested number of permits is negative or zero + */ + // CHANGELOG: return value changed from boolean to Optional + public Optional tryAcquire(int permits, long timeout, TimeUnit unit) { + long timeoutMicros = max(unit.toMicros(timeout), 0); + checkPermits(permits); + long microsToWait; + synchronized (mutex()) { + long nowMicros = stopwatch.readMicros(); + if (!canAcquire(nowMicros, timeoutMicros)) { + // CHANGELOG: return value changed from false to Optional#empty + return Optional.empty(); + } else { + microsToWait = reserveAndGetWaitLength(permits, nowMicros); + } + } + stopwatch.sleepMicrosUninterruptibly(microsToWait); + // CHANGELOG: return value changed from true to Optional + return Optional.of(Duration.ofNanos(TimeUnit.MICROSECONDS.toNanos(microsToWait))); + } + + // CHANGELOG: new method + /** + * Immediately steals the given number of permits. This will potentially penalize future callers, but has no + * effect on callers that are already waiting for permits. + */ + public void steal(int permits) { + reserve(permits); + } + + private boolean canAcquire(long nowMicros, long timeoutMicros) { + return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros; + } + + /** + * Reserves next ticket and returns the wait time that the caller must wait for. + * + * @return the required wait time, never negative + */ + final long reserveAndGetWaitLength(int permits, long nowMicros) { + long momentAvailable = reserveEarliestAvailable(permits, nowMicros); + return max(momentAvailable - nowMicros, 0); + } + + /** + * Returns the earliest time that permits are available (with one caveat). + * + * @return the time that permits are available, or, if permits are available immediately, an + * arbitrary past or present time + */ + abstract long queryEarliestAvailable(long nowMicros); + + /** + * Reserves the requested number of permits and returns the time that those permits can be used + * (with one caveat). + * + * @return the time that the permits may be used, or, if the permits may be used immediately, an + * arbitrary past or present time + */ + abstract long reserveEarliestAvailable(int permits, long nowMicros); + + @Override + public String toString() { + return String.format(Locale.ROOT, "RateLimiter[stableRate=%3.1fqps]", getRate()); + } + + @VisibleForTesting + // CHANGELOG: modifier changed from package private to public + public abstract static class SleepingStopwatch { + /* + * We always hold the mutex when calling this. TODO(cpovirk): Is that important? Perhaps we need + * to guarantee that each call to reserveEarliestAvailable, etc. sees a value >= the previous? + * Also, is it OK that we don't hold the mutex when sleeping? + */ + // CHANGELOG: modifier changed from package private to public + public abstract long readMicros(); + + abstract void sleepMicrosUninterruptibly(long micros); + + // CHANGELOG: modifier changed from package private to public + public static final SleepingStopwatch createFromSystemTimer() { + return new SleepingStopwatch() { + final Stopwatch stopwatch = Stopwatch.createStarted(); + + @Override + // CHANGELOG: modifier changed from package private to public + public long readMicros() { + return stopwatch.elapsed(MICROSECONDS); + } + + @Override + void sleepMicrosUninterruptibly(long micros) { + if (micros > 0) { + Uninterruptibles.sleepUninterruptibly(micros, MICROSECONDS); + } + } + }; + } + } + + private static int checkPermits(int permits) { + checkArgument(permits > 0, "Requested permits (%s) must be positive", permits); + return permits; + } +} diff --git a/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/guava/SmoothRateLimiter.java b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/guava/SmoothRateLimiter.java new file mode 100644 index 00000000000..e8124d6a1db --- /dev/null +++ b/qos-service-impl/src/main/java/com/palantir/atlasdb/qos/ratelimit/guava/SmoothRateLimiter.java @@ -0,0 +1,401 @@ +//CHECKSTYLE:OFF +/* + * Copyright (C) 2012 The Guava 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. + */ + +// CHANGELOG: package name changed +package com.palantir.atlasdb.qos.ratelimit.guava; + +import static java.lang.Math.min; +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.util.concurrent.TimeUnit; + +import com.google.common.math.LongMath; + +// CHANGELOG: modifier changed from package private to public +public abstract class SmoothRateLimiter extends RateLimiter { + /* + * How is the RateLimiter designed, and why? + * + * The primary feature of a RateLimiter is its "stable rate", the maximum rate that + * is should allow at normal conditions. This is enforced by "throttling" incoming + * requests as needed, i.e. compute, for an incoming request, the appropriate throttle time, + * and make the calling thread wait as much. + * + * The simplest way to maintain a rate of QPS is to keep the timestamp of the last + * granted request, and ensure that (1/QPS) seconds have elapsed since then. For example, + * for a rate of QPS=5 (5 tokens per second), if we ensure that a request isn't granted + * earlier than 200ms after the last one, then we achieve the intended rate. + * If a request comes and the last request was granted only 100ms ago, then we wait for + * another 100ms. At this rate, serving 15 fresh permits (i.e. for an acquire(15) request) + * naturally takes 3 seconds. + * + * It is important to realize that such a RateLimiter has a very superficial memory + * of the past: it only remembers the last request. What if the RateLimiter was unused for + * a long period of time, then a request arrived and was immediately granted? + * This RateLimiter would immediately forget about that past underutilization. This may + * result in either underutilization or overflow, depending on the real world consequences + * of not using the expected rate. + * + * Past underutilization could mean that excess resources are available. Then, the RateLimiter + * should speed up for a while, to take advantage of these resources. This is important + * when the rate is applied to networking (limiting bandwidth), where past underutilization + * typically translates to "almost empty buffers", which can be filled immediately. + * + * On the other hand, past underutilization could mean that "the server responsible for + * handling the request has become less ready for future requests", i.e. its caches become + * stale, and requests become more likely to trigger expensive operations (a more extreme + * case of this example is when a server has just booted, and it is mostly busy with getting + * itself up to speed). + * + * To deal with such scenarios, we add an extra dimension, that of "past underutilization", + * modeled by "storedPermits" variable. This variable is zero when there is no + * underutilization, and it can grow up to maxStoredPermits, for sufficiently large + * underutilization. So, the requested permits, by an invocation acquire(permits), + * are served from: + * - stored permits (if available) + * - fresh permits (for any remaining permits) + * + * How this works is best explained with an example: + * + * For a RateLimiter that produces 1 token per second, every second + * that goes by with the RateLimiter being unused, we increase storedPermits by 1. + * Say we leave the RateLimiter unused for 10 seconds (i.e., we expected a request at time + * X, but we are at time X + 10 seconds before a request actually arrives; this is + * also related to the point made in the last paragraph), thus storedPermits + * becomes 10.0 (assuming maxStoredPermits >= 10.0). At that point, a request of acquire(3) + * arrives. We serve this request out of storedPermits, and reduce that to 7.0 (how this is + * translated to throttling time is discussed later). Immediately after, assume that an + * acquire(10) request arriving. We serve the request partly from storedPermits, + * using all the remaining 7.0 permits, and the remaining 3.0, we serve them by fresh permits + * produced by the rate limiter. + * + * We already know how much time it takes to serve 3 fresh permits: if the rate is + * "1 token per second", then this will take 3 seconds. But what does it mean to serve 7 + * stored permits? As explained above, there is no unique answer. If we are primarily + * interested to deal with underutilization, then we want stored permits to be given out + * /faster/ than fresh ones, because underutilization = free resources for the taking. + * If we are primarily interested to deal with overflow, then stored permits could + * be given out /slower/ than fresh ones. Thus, we require a (different in each case) + * function that translates storedPermits to throtting time. + * + * This role is played by storedPermitsToWaitTime(double storedPermits, double permitsToTake). + * The underlying model is a continuous function mapping storedPermits + * (from 0.0 to maxStoredPermits) onto the 1/rate (i.e. intervals) that is effective at the given + * storedPermits. "storedPermits" essentially measure unused time; we spend unused time + * buying/storing permits. Rate is "permits / time", thus "1 / rate = time / permits". + * Thus, "1/rate" (time / permits) times "permits" gives time, i.e., integrals on this + * function (which is what storedPermitsToWaitTime() computes) correspond to minimum intervals + * between subsequent requests, for the specified number of requested permits. + * + * Here is an example of storedPermitsToWaitTime: + * If storedPermits == 10.0, and we want 3 permits, we take them from storedPermits, + * reducing them to 7.0, and compute the throttling for these as a call to + * storedPermitsToWaitTime(storedPermits = 10.0, permitsToTake = 3.0), which will + * evaluate the integral of the function from 7.0 to 10.0. + * + * Using integrals guarantees that the effect of a single acquire(3) is equivalent + * to { acquire(1); acquire(1); acquire(1); }, or { acquire(2); acquire(1); }, etc, + * since the integral of the function in [7.0, 10.0] is equivalent to the sum of the + * integrals of [7.0, 8.0], [8.0, 9.0], [9.0, 10.0] (and so on), no matter + * what the function is. This guarantees that we handle correctly requests of varying weight + * (permits), /no matter/ what the actual function is - so we can tweak the latter freely. + * (The only requirement, obviously, is that we can compute its integrals). + * + * Note well that if, for this function, we chose a horizontal line, at height of exactly + * (1/QPS), then the effect of the function is non-existent: we serve storedPermits at + * exactly the same cost as fresh ones (1/QPS is the cost for each). We use this trick later. + * + * If we pick a function that goes /below/ that horizontal line, it means that we reduce + * the area of the function, thus time. Thus, the RateLimiter becomes /faster/ after a + * period of underutilization. If, on the other hand, we pick a function that + * goes /above/ that horizontal line, then it means that the area (time) is increased, + * thus storedPermits are more costly than fresh permits, thus the RateLimiter becomes + * /slower/ after a period of underutilization. + * + * Last, but not least: consider a RateLimiter with rate of 1 permit per second, currently + * completely unused, and an expensive acquire(100) request comes. It would be nonsensical + * to just wait for 100 seconds, and /then/ start the actual task. Why wait without doing + * anything? A much better approach is to /allow/ the request right away (as if it was an + * acquire(1) request instead), and postpone /subsequent/ requests as needed. In this version, + * we allow starting the task immediately, and postpone by 100 seconds future requests, + * thus we allow for work to get done in the meantime instead of waiting idly. + * + * This has important consequences: it means that the RateLimiter doesn't remember the time + * of the _last_ request, but it remembers the (expected) time of the _next_ request. This + * also enables us to tell immediately (see tryAcquire(timeout)) whether a particular + * timeout is enough to get us to the point of the next scheduling time, since we always + * maintain that. And what we mean by "an unused RateLimiter" is also defined by that + * notion: when we observe that the "expected arrival time of the next request" is actually + * in the past, then the difference (now - past) is the amount of time that the RateLimiter + * was formally unused, and it is that amount of time which we translate to storedPermits. + * (We increase storedPermits with the amount of permits that would have been produced + * in that idle time). So, if rate == 1 permit per second, and arrivals come exactly + * one second after the previous, then storedPermits is _never_ increased -- we would only + * increase it for arrivals _later_ than the expected one second. + */ + + /** + * This implements the following function where coldInterval = coldFactor * stableInterval. + * + * ^ throttling + * | + * cold + / + * interval | /. + * | / . + * | / . <-- "warmup period" is the area of the trapezoid between + * | / . thresholdPermits and maxPermits + * | / . + * | / . + * | / . + * stable +----------/ WARM . + * interval | . UP . + * | . PERIOD. + * | . . + * 0 +----------+-------+--------------> storedPermits + * 0 thresholdPermits maxPermits + * Before going into the details of this particular function, let's keep in mind the basics: + * 1) The state of the RateLimiter (storedPermits) is a vertical line in this figure. + * 2) When the RateLimiter is not used, this goes right (up to maxPermits) + * 3) When the RateLimiter is used, this goes left (down to zero), since if we have storedPermits, + * we serve from those first + * 4) When _unused_, we go right at a constant rate! The rate at which we move to + * the right is chosen as maxPermits / warmupPeriod. This ensures that the time it takes to + * go from 0 to maxPermits is equal to warmupPeriod. + * 5) When _used_, the time it takes, as explained in the introductory class note, is + * equal to the integral of our function, between X permits and X-K permits, assuming + * we want to spend K saved permits. + * + * In summary, the time it takes to move to the left (spend K permits), is equal to the + * area of the function of width == K. + * + * Assuming we have saturated demand, the time to go from maxPermits to thresholdPermits is + * equal to warmupPeriod. And the time to go from thresholdPermits to 0 is + * warmupPeriod/2. (The reason that this is warmupPeriod/2 is to maintain the behavior of + * the original implementation where coldFactor was hard coded as 3.) + * + * It remains to calculate thresholdsPermits and maxPermits. + * + * - The time to go from thresholdPermits to 0 is equal to the integral of the function between + * 0 and thresholdPermits. This is thresholdPermits * stableIntervals. By (5) it is also + * equal to warmupPeriod/2. Therefore + * + * thresholdPermits = 0.5 * warmupPeriod / stableInterval. + * + * - The time to go from maxPermits to thresholdPermits is equal to the integral of the function + * between thresholdPermits and maxPermits. This is the area of the pictured trapezoid, and it + * is equal to 0.5 * (stableInterval + coldInterval) * (maxPermits - thresholdPermits). It is + * also equal to warmupPeriod, so + * + * maxPermits = thresholdPermits + 2 * warmupPeriod / (stableInterval + coldInterval). + */ + static final class SmoothWarmingUp extends SmoothRateLimiter { + private final long warmupPeriodMicros; + /** + * The slope of the line from the stable interval (when permits == 0), to the cold interval + * (when permits == maxPermits) + */ + private double slope; + private double thresholdPermits; + private double coldFactor; + + SmoothWarmingUp( + SleepingStopwatch stopwatch, long warmupPeriod, TimeUnit timeUnit, double coldFactor) { + super(stopwatch); + this.warmupPeriodMicros = timeUnit.toMicros(warmupPeriod); + this.coldFactor = coldFactor; + } + + @Override + void doSetRate(double permitsPerSecond, double stableIntervalMicros) { + double oldMaxPermits = maxPermits; + double coldIntervalMicros = stableIntervalMicros * coldFactor; + thresholdPermits = 0.5 * warmupPeriodMicros / stableIntervalMicros; + maxPermits = thresholdPermits + + 2.0 * warmupPeriodMicros / (stableIntervalMicros + coldIntervalMicros); + slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits); + if (oldMaxPermits == Double.POSITIVE_INFINITY) { + // if we don't special-case this, we would get storedPermits == NaN, below + storedPermits = 0.0; + } else { + storedPermits = (oldMaxPermits == 0.0) + ? maxPermits // initial state is cold + : storedPermits * maxPermits / oldMaxPermits; + } + } + + @Override + long storedPermitsToWaitTime(double storedPermits, double permitsToTake) { + double availablePermitsAboveThreshold = storedPermits - thresholdPermits; + long micros = 0; + // measuring the integral on the right part of the function (the climbing line) + if (availablePermitsAboveThreshold > 0.0) { + double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake); + micros = (long) (permitsAboveThresholdToTake + * (permitsToTime(availablePermitsAboveThreshold) + + permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake)) / 2.0); + permitsToTake -= permitsAboveThresholdToTake; + } + // measuring the integral on the left part of the function (the horizontal line) + micros += (stableIntervalMicros * permitsToTake); + return micros; + } + + private double permitsToTime(double permits) { + return stableIntervalMicros + permits * slope; + } + + @Override + double coolDownIntervalMicros() { + return warmupPeriodMicros / maxPermits; + } + } + + /** + * This implements a "bursty" RateLimiter, where storedPermits are translated to + * zero throttling. The maximum number of permits that can be saved (when the RateLimiter is + * unused) is defined in terms of time, in this sense: if a RateLimiter is 2qps, and this + * time is specified as 10 seconds, we can save up to 2 * 10 = 20 permits. + */ + // CHANGELOG: modifier changed from package private to public + public static final class SmoothBursty extends SmoothRateLimiter { + /** The work (permits) of how many seconds can be saved up if this RateLimiter is unused? */ + final double maxBurstSeconds; + + // CHANGELOG: modifier changed from package private to public + public SmoothBursty(SleepingStopwatch stopwatch, double maxBurstSeconds) { + super(stopwatch); + this.maxBurstSeconds = maxBurstSeconds; + } + + @Override + void doSetRate(double permitsPerSecond, double stableIntervalMicros) { + double oldMaxPermits = this.maxPermits; + maxPermits = maxBurstSeconds * permitsPerSecond; + if (oldMaxPermits == Double.POSITIVE_INFINITY) { + // if we don't special-case this, we would get storedPermits == NaN, below + storedPermits = maxPermits; + } else { + storedPermits = (oldMaxPermits == 0.0) + ? 0.0 // initial state + : storedPermits * maxPermits / oldMaxPermits; + } + } + + @Override + long storedPermitsToWaitTime(double storedPermits, double permitsToTake) { + return 0L; + } + + @Override + double coolDownIntervalMicros() { + return stableIntervalMicros; + } + } + + /** + * The currently stored permits. + */ + double storedPermits; + + /** + * The maximum number of stored permits. + */ + double maxPermits; + + /** + * The interval between two unit requests, at our stable rate. E.g., a stable rate of 5 permits + * per second has a stable interval of 200ms. + */ + double stableIntervalMicros; + + /** + * The time when the next request (no matter its size) will be granted. After granting a + * request, this is pushed further in the future. Large requests push this further than small + * requests. + */ + private long nextFreeTicketMicros = 0L; // could be either in the past or future + + private SmoothRateLimiter(SleepingStopwatch stopwatch) { + super(stopwatch); + } + + @Override + final void doSetRate(double permitsPerSecond, long nowMicros) { + resync(nowMicros); + double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond; + this.stableIntervalMicros = stableIntervalMicros; + doSetRate(permitsPerSecond, stableIntervalMicros); + } + + abstract void doSetRate(double permitsPerSecond, double stableIntervalMicros); + + @Override + final double doGetRate() { + return SECONDS.toMicros(1L) / stableIntervalMicros; + } + + @Override + final long queryEarliestAvailable(long nowMicros) { + return nextFreeTicketMicros; + } + + @Override + final long reserveEarliestAvailable(int requiredPermits, long nowMicros) { + resync(nowMicros); + long returnValue = nextFreeTicketMicros; + double storedPermitsToSpend = min(requiredPermits, this.storedPermits); + double freshPermits = requiredPermits - storedPermitsToSpend; + long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend) + + (long) (freshPermits * stableIntervalMicros); + + try { + this.nextFreeTicketMicros = LongMath.checkedAdd(nextFreeTicketMicros, waitMicros); + } catch (ArithmeticException e) { + this.nextFreeTicketMicros = Long.MAX_VALUE; + } + this.storedPermits -= storedPermitsToSpend; + return returnValue; + } + + /** + * Translates a specified portion of our currently stored permits which we want to + * spend/acquire, into a throttling time. Conceptually, this evaluates the integral + * of the underlying function we use, for the range of + * [(storedPermits - permitsToTake), storedPermits]. + * + *

This always holds: {@code 0 <= permitsToTake <= storedPermits} + */ + abstract long storedPermitsToWaitTime(double storedPermits, double permitsToTake); + + /** + * Returns the number of microseconds during cool down that we have to wait to get a new permit. + */ + abstract double coolDownIntervalMicros(); + + /** + * Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time. + */ + void resync(long nowMicros) { + // if nextFreeTicket is in the past, resync to now + if (nowMicros > nextFreeTicketMicros) { + storedPermits = min(maxPermits, + storedPermits + + (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros()); + nextFreeTicketMicros = nowMicros; + } + } +} diff --git a/qos-service-impl/src/test/java/com/palantir/atlasdb/qos/AtlasDbQosClientTest.java b/qos-service-impl/src/test/java/com/palantir/atlasdb/qos/AtlasDbQosClientTest.java deleted file mode 100644 index 1c5b08ad46d..00000000000 --- a/qos-service-impl/src/test/java/com/palantir/atlasdb/qos/AtlasDbQosClientTest.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2017 Palantir Technologies, Inc. All rights reserved. - * - * Licensed under the BSD-3 License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://opensource.org/licenses/BSD-3-Clause - * - * 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 com.palantir.atlasdb.qos; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.concurrent.TimeUnit; - -import org.jmock.lib.concurrent.DeterministicScheduler; -import org.junit.Before; -import org.junit.Test; - -import com.palantir.atlasdb.qos.client.AtlasDbQosClient; -import com.palantir.atlasdb.qos.ratelimit.QosRateLimiter; - -public class AtlasDbQosClientTest { - private QosService qosService = mock(QosService.class); - private DeterministicScheduler scheduler = new DeterministicScheduler(); - private QosRateLimiter rateLimiter = mock(QosRateLimiter.class); - - @Before - public void setUp() { - when(qosService.getLimit("test-client")).thenReturn(1L); - } - - @Test - public void doesNotBackOff() { - AtlasDbQosClient qosClient = new AtlasDbQosClient(qosService, scheduler, "test-client", rateLimiter); - scheduler.tick(1L, TimeUnit.MILLISECONDS); - qosClient.checkLimit(); - } - - @Test - public void throwsAfterLimitExceeded() { - AtlasDbQosClient qosClient = new AtlasDbQosClient(qosService, scheduler, "test-client", rateLimiter); - scheduler.tick(1L, TimeUnit.MILLISECONDS); - qosClient.checkLimit(); - - assertThatThrownBy(qosClient::checkLimit).isInstanceOf(RuntimeException.class); - } - - @Test - public void canCheckAgainAfterRefreshPeriod() { - AtlasDbQosClient qosClient = new AtlasDbQosClient(qosService, scheduler, "test-client", rateLimiter); - scheduler.tick(1L, TimeUnit.MILLISECONDS); - qosClient.checkLimit(); - - assertThatThrownBy(qosClient::checkLimit) - .isInstanceOf(RuntimeException.class).hasMessage("Rate limit exceeded"); - - scheduler.tick(60L, TimeUnit.SECONDS); - - qosClient.checkLimit(); - } -} diff --git a/qos-service-impl/src/test/java/com/palantir/atlasdb/qos/QosClientConfigDeserializationTest.java b/qos-service-impl/src/test/java/com/palantir/atlasdb/qos/QosClientConfigDeserializationTest.java new file mode 100644 index 00000000000..1edfe1e9b16 --- /dev/null +++ b/qos-service-impl/src/test/java/com/palantir/atlasdb/qos/QosClientConfigDeserializationTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the BSD-3 License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/BSD-3-Clause + * + * 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 com.palantir.atlasdb.qos; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; + +import org.junit.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.datatype.guava.GuavaModule; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.palantir.atlasdb.qos.config.ImmutableQosClientConfig; +import com.palantir.atlasdb.qos.config.ImmutableQosLimitsConfig; +import com.palantir.atlasdb.qos.config.QosClientConfig; +import com.palantir.atlasdb.qos.config.QosServiceRuntimeConfig; +import com.palantir.remoting.api.config.service.HumanReadableDuration; +import com.palantir.remoting.api.config.service.ServiceConfiguration; +import com.palantir.remoting.api.config.ssl.SslConfiguration; +import com.palantir.remoting3.ext.jackson.ShimJdk7Module; + +public class QosClientConfigDeserializationTest { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(new YAMLFactory()) + .registerModule(new GuavaModule()) + .registerModule(new ShimJdk7Module()) + .registerModule(new Jdk8Module()); + + @Test + public void canDeserializeFromYaml() throws IOException { + QosClientConfig expected = ImmutableQosClientConfig.builder() + .qosService( + ServiceConfiguration.builder() + .addUris("http://localhost:8080") + .security(SslConfiguration.of(Paths.get("trustStore.jks"))) + .build()) + .maxBackoffSleepTime(HumanReadableDuration.seconds(20)) + .limits(ImmutableQosLimitsConfig.builder() + .readBytesPerSecond(123) + .writeBytesPerSecond(456) + .build()) + .build(); + + File configFile = new File(QosServiceRuntimeConfig.class.getResource("/qos-client.yml").getPath()); + QosClientConfig config = OBJECT_MAPPER.readValue(configFile, QosClientConfig.class); + + assertThat(config).isEqualTo(expected); + } + +} diff --git a/qos-service-impl/src/test/java/com/palantir/atlasdb/qos/QosRuntimeConfigDeserializationTest.java b/qos-service-impl/src/test/java/com/palantir/atlasdb/qos/QosRuntimeConfigDeserializationTest.java index ada189ff4b4..9edcedafa2b 100644 --- a/qos-service-impl/src/test/java/com/palantir/atlasdb/qos/QosRuntimeConfigDeserializationTest.java +++ b/qos-service-impl/src/test/java/com/palantir/atlasdb/qos/QosRuntimeConfigDeserializationTest.java @@ -37,7 +37,7 @@ public class QosRuntimeConfigDeserializationTest { @Test public void canDeserializeQosServerConfiguration() throws IOException { - File testConfigFile = new File(QosServiceRuntimeConfig.class.getResource("/qos.yml").getPath()); + File testConfigFile = new File(QosServiceRuntimeConfig.class.getResource("/qos-server.yml").getPath()); QosServiceRuntimeConfig configuration = OBJECT_MAPPER.readValue(testConfigFile, QosServiceRuntimeConfig.class); assertThat(configuration).isEqualTo(ImmutableQosServiceRuntimeConfig.builder() diff --git a/qos-service-impl/src/test/java/com/palantir/atlasdb/qos/client/AtlasDbQosClientTest.java b/qos-service-impl/src/test/java/com/palantir/atlasdb/qos/client/AtlasDbQosClientTest.java new file mode 100644 index 00000000000..b0b00f2b710 --- /dev/null +++ b/qos-service-impl/src/test/java/com/palantir/atlasdb/qos/client/AtlasDbQosClientTest.java @@ -0,0 +1,196 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the BSD-3 License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/BSD-3-Clause + * + * 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 com.palantir.atlasdb.qos.client; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.time.Duration; + +import org.junit.Before; +import org.junit.Test; + +import com.google.common.base.Ticker; +import com.palantir.atlasdb.qos.ImmutableQueryWeight; +import com.palantir.atlasdb.qos.QosClient; +import com.palantir.atlasdb.qos.QueryWeight; +import com.palantir.atlasdb.qos.metrics.QosMetrics; +import com.palantir.atlasdb.qos.ratelimit.ImmutableQosRateLimiters; +import com.palantir.atlasdb.qos.ratelimit.QosRateLimiter; +import com.palantir.atlasdb.qos.ratelimit.QosRateLimiters; +import com.palantir.atlasdb.qos.ratelimit.RateLimitExceededException; + +public class AtlasDbQosClientTest { + + private static final int ESTIMATED_BYTES = 10; + private static final int ACTUAL_BYTES = 51; + private static final long START_NANOS = 1100L; + private static final long END_NANOS = 5500L; + private static final long TOTAL_NANOS = END_NANOS - START_NANOS; + + private static final QueryWeight ESTIMATED_WEIGHT = ImmutableQueryWeight.builder() + .numBytes(ESTIMATED_BYTES) + .numDistinctRows(1) + .timeTakenNanos((int) TOTAL_NANOS) + .build(); + + private static final QueryWeight ACTUAL_WEIGHT = ImmutableQueryWeight.builder() + .numBytes(ACTUAL_BYTES) + .numDistinctRows(10) + .timeTakenNanos((int) TOTAL_NANOS) + .build(); + + private QosClient.QueryWeigher weigher = mock(QosClient.QueryWeigher.class); + + private QosRateLimiter readLimiter = mock(QosRateLimiter.class); + private QosRateLimiter writeLimiter = mock(QosRateLimiter.class); + private QosRateLimiters rateLimiters = ImmutableQosRateLimiters.builder() + .read(readLimiter).write(writeLimiter).build(); + private QosMetrics metrics = mock(QosMetrics.class); + private Ticker ticker = mock(Ticker.class); + + private AtlasDbQosClient qosClient = new AtlasDbQosClient(rateLimiters, metrics, ticker); + + @Before + public void setUp() { + when(ticker.read()).thenReturn(START_NANOS).thenReturn(END_NANOS); + + when(weigher.estimate()).thenReturn(ESTIMATED_WEIGHT); + when(weigher.weighSuccess(any(), anyLong())).thenReturn(ACTUAL_WEIGHT); + when(weigher.weighFailure(any(), anyLong())).thenReturn(ACTUAL_WEIGHT); + + when(readLimiter.consumeWithBackoff(anyLong())).thenReturn(Duration.ZERO); + when(writeLimiter.consumeWithBackoff(anyLong())).thenReturn(Duration.ZERO); + } + + @Test + public void consumesSpecifiedNumUnitsForReads() { + qosClient.executeRead(() -> "foo", weigher); + + verify(readLimiter).consumeWithBackoff(ESTIMATED_BYTES); + verify(readLimiter).recordAdjustment(ACTUAL_BYTES - ESTIMATED_BYTES); + verifyNoMoreInteractions(readLimiter, writeLimiter); + } + + @Test + public void recordsReadMetrics() throws TestCheckedException { + qosClient.executeRead(() -> "foo", weigher); + + verify(metrics).recordRead(ACTUAL_WEIGHT); + } + + @Test + public void passesResultAndTimeToReadWeigher() throws TestCheckedException { + qosClient.executeRead(() -> "foo", weigher); + + verify(weigher).weighSuccess("foo", TOTAL_NANOS); + } + + @Test + public void consumesSpecifiedNumUnitsForWrites() { + qosClient.executeWrite(() -> null, weigher); + + verify(writeLimiter).consumeWithBackoff(ESTIMATED_BYTES); + verify(writeLimiter).recordAdjustment(ACTUAL_BYTES - ESTIMATED_BYTES); + verifyNoMoreInteractions(readLimiter, writeLimiter); + } + + @Test + public void recordsWriteMetrics() throws TestCheckedException { + qosClient.executeWrite(() -> null, weigher); + + verify(metrics).recordWrite(ACTUAL_WEIGHT); + } + + @Test + public void recordsReadMetricsOnFailure() throws TestCheckedException { + TestCheckedException error = new TestCheckedException(); + assertThatThrownBy(() -> qosClient.executeRead(() -> { + throw error; + }, weigher)).isInstanceOf(TestCheckedException.class); + + verify(metrics).recordRead(ACTUAL_WEIGHT); + } + + @Test + public void recordsWriteMetricsOnFailure() throws TestCheckedException { + TestCheckedException error = new TestCheckedException(); + assertThatThrownBy(() -> qosClient.executeWrite(() -> { + throw error; + }, weigher)).isInstanceOf(TestCheckedException.class); + + verify(metrics).recordWrite(ACTUAL_WEIGHT); + } + + @Test + public void passesExceptionToWeigherOnFailure() throws TestCheckedException { + TestCheckedException error = new TestCheckedException(); + assertThatThrownBy(() -> qosClient.executeRead(() -> { + throw error; + }, weigher)).isInstanceOf(TestCheckedException.class); + + verify(weigher).weighFailure(error, TOTAL_NANOS); + verify(weigher, never()).weighSuccess(any(), anyLong()); + } + + @Test + public void propagatesCheckedExceptions() throws TestCheckedException { + assertThatThrownBy(() -> qosClient.executeRead(() -> { + throw new TestCheckedException(); + }, weigher)).isInstanceOf(TestCheckedException.class); + + assertThatThrownBy(() -> qosClient.executeWrite(() -> { + throw new TestCheckedException(); + }, weigher)).isInstanceOf(TestCheckedException.class); + + verify(metrics, never()).recordRateLimitedException(); + } + + @Test + public void recordsBackoffTime() { + when(readLimiter.consumeWithBackoff(anyLong())).thenReturn(Duration.ofMillis(1_100)); + qosClient.executeRead(() -> "foo", weigher); + + verify(metrics).recordBackoffMicros(1_100_000); + } + + @Test + public void recordsBackoffExceptions() { + when(readLimiter.consumeWithBackoff(anyLong())).thenThrow(new RateLimitExceededException("rate limited")); + assertThatThrownBy(() -> qosClient.executeRead(() -> "foo", weigher)).isInstanceOf( + RateLimitExceededException.class); + + verify(metrics).recordRateLimitedException(); + } + + @Test + public void doesNotRecordRuntimeExceptions() { + when(readLimiter.consumeWithBackoff(anyLong())).thenThrow(new RuntimeException("foo")); + assertThatThrownBy(() -> qosClient.executeRead(() -> "foo", weigher)).isInstanceOf( + RuntimeException.class); + + verify(metrics, never()).recordRateLimitedException(); + } + + static class TestCheckedException extends Exception {} +} diff --git a/qos-service-impl/src/test/java/com/palantir/atlasdb/qos/ratelimit/QosRateLimiterTest.java b/qos-service-impl/src/test/java/com/palantir/atlasdb/qos/ratelimit/QosRateLimiterTest.java index 8d88ad58b21..dbc2bbcd185 100644 --- a/qos-service-impl/src/test/java/com/palantir/atlasdb/qos/ratelimit/QosRateLimiterTest.java +++ b/qos-service-impl/src/test/java/com/palantir/atlasdb/qos/ratelimit/QosRateLimiterTest.java @@ -23,72 +23,108 @@ import java.time.Duration; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import org.junit.Before; import org.junit.Test; +import com.palantir.atlasdb.qos.ratelimit.guava.RateLimiter; + public class QosRateLimiterTest { private static final long START_TIME_MICROS = 0L; + private static final Supplier MAX_BACKOFF_TIME_MILLIS = () -> 10_000L; RateLimiter.SleepingStopwatch stopwatch = mock(RateLimiter.SleepingStopwatch.class); - QosRateLimiter limiter = new QosRateLimiter(stopwatch); + Supplier currentRate = mock(Supplier.class); + QosRateLimiter limiter; @Before public void before() { when(stopwatch.readMicros()).thenReturn(START_TIME_MICROS); + when(currentRate.get()).thenReturn(10L); + + limiter = new QosRateLimiter(stopwatch, MAX_BACKOFF_TIME_MILLIS, currentRate); } @Test - public void doesNotLimitIfNoLimitIsSet() { + public void doesNotLimitIfLimitIsVeryHigh() { + when(currentRate.get()).thenReturn(Long.MAX_VALUE); + assertThat(limiter.consumeWithBackoff(Integer.MAX_VALUE)).isEqualTo(Duration.ZERO); assertThat(limiter.consumeWithBackoff(Integer.MAX_VALUE)).isEqualTo(Duration.ZERO); assertThat(limiter.consumeWithBackoff(Integer.MAX_VALUE)).isEqualTo(Duration.ZERO); } @Test - public void limitsBySleepingIfTimeIsReasonable() { - limiter.updateRate(10); + public void limitsOnlyWhenConsumptionExceedsLimit() { + when(currentRate.get()).thenReturn(100L); + limiter.consumeWithBackoff(1); // set the current time + + tickMillis(500); + + assertThat(limiter.consumeWithBackoff(25L)).isEqualTo(Duration.ZERO); + assertThat(limiter.consumeWithBackoff(25L)).isEqualTo(Duration.ZERO); + + tickMillis(500); + + assertThat(limiter.consumeWithBackoff(20L)).isEqualTo(Duration.ZERO); + tickMillis(500); + + assertThat(limiter.consumeWithBackoff(20L)).isEqualTo(Duration.ZERO); + assertThat(limiter.consumeWithBackoff(20L)).isEqualTo(Duration.ZERO); + assertThat(limiter.consumeWithBackoff(40L)).isEqualTo(Duration.ZERO); + + assertThat(limiter.consumeWithBackoff(40L)).isGreaterThan(Duration.ZERO); + } + + @Test + public void limitsBySleepingIfTimeIsReasonable() { assertThat(limiter.consumeWithBackoff(100)).isEqualTo(Duration.ZERO); assertThat(limiter.consumeWithBackoff(1)).isGreaterThan(Duration.ZERO); } @Test public void limitsByThrowingIfSleepTimeIsTooGreat() { - limiter.updateRate(10); limiter.consumeWithBackoff(1_000); assertThatThrownBy(() -> limiter.consumeWithBackoff(100)) - .hasMessageContaining("rate limited"); + .isInstanceOf(RateLimitExceededException.class) + .hasMessageContaining("Rate limited"); } @Test - public void consumingAdditionalUnitsPenalizesFutureCallers() { - limiter.updateRate(10); + public void doesNotThrowIfMaxBackoffTimeIsVeryLarge() { + QosRateLimiter limiterWithLargeBackoffLimit = new QosRateLimiter(stopwatch, () -> Long.MAX_VALUE, () -> 10L); + + limiterWithLargeBackoffLimit.consumeWithBackoff(1_000_000_000); + limiterWithLargeBackoffLimit.consumeWithBackoff(1_000_000_000); + } + @Test + public void consumingAdditionalUnitsPenalizesFutureCallers() { limiter.consumeWithBackoff(1); - limiter.recordAdjustment(100); + limiter.recordAdjustment(25); assertThat(limiter.consumeWithBackoff(1)).isGreaterThan(Duration.ZERO); } @Test public void canConsumeBurstUnits() { - limiter.updateRate(10); limiter.consumeWithBackoff(100); // simulate 30 seconds passing with no consumption - when(stopwatch.readMicros()).thenReturn(TimeUnit.SECONDS.toMicros(30)); + tickMillis(30_000); assertThat(limiter.consumeWithBackoff(10)).isEqualTo(Duration.ZERO); - assertThat(limiter.consumeWithBackoff(10)).isEqualTo(Duration.ZERO); + assertThat(limiter.consumeWithBackoff(20)).isEqualTo(Duration.ZERO); assertThat(limiter.consumeWithBackoff(10)).isEqualTo(Duration.ZERO); } @Test public void canConsumeImmediatelyAgainAfterBackoff() { - limiter.updateRate(10); + when(currentRate.get()).thenReturn(10L); limiter.consumeWithBackoff(100); Duration timeWaited = limiter.consumeWithBackoff(20); @@ -101,11 +137,37 @@ public void canConsumeImmediatelyAgainAfterBackoff() { @Test public void sleepTimeIsSensible() { - limiter.updateRate(10); - limiter.consumeWithBackoff(100); + limiter.consumeWithBackoff(50); assertThat(limiter.consumeWithBackoff(20)).isEqualTo(Duration.ofSeconds(5)); assertThat(limiter.consumeWithBackoff(20)).isEqualTo(Duration.ofSeconds(7)); } + @Test + public void canUpdateRate() { + // baseline + limiter.consumeWithBackoff(20); + assertThat(limiter.consumeWithBackoff(20)).isGreaterThan(Duration.ZERO); + + // increase to a large rate + when(currentRate.get()).thenReturn(1000000L); + limiter.consumeWithBackoff(1); + tickMillis(1); + + assertThat(limiter.consumeWithBackoff(50)).isEqualTo(Duration.ZERO); + assertThat(limiter.consumeWithBackoff(500)).isEqualTo(Duration.ZERO); + + // decrease to small rate + when(currentRate.get()).thenReturn(10L); + tickMillis(1000); + + limiter.consumeWithBackoff(1); + limiter.consumeWithBackoff(20); + assertThat(limiter.consumeWithBackoff(20)).isGreaterThan(Duration.ZERO); + } + + private void tickMillis(long millis) { + long now = stopwatch.readMicros(); + when(stopwatch.readMicros()).thenReturn(now + millis * 1_000); + } } diff --git a/qos-service-impl/src/test/resources/qos-client.yml b/qos-service-impl/src/test/resources/qos-client.yml new file mode 100644 index 00000000000..163da250c04 --- /dev/null +++ b/qos-service-impl/src/test/resources/qos-client.yml @@ -0,0 +1,10 @@ +qosService: + uris: + - http://localhost:8080 + security: + trustStorePath: trustStore.jks +maxBackoffSleepTime: 20 seconds +limits: + readBytesPerSecond: 123 + writeBytesPerSecond: 456 + diff --git a/qos-service-impl/src/test/resources/qos.yml b/qos-service-impl/src/test/resources/qos-server.yml similarity index 100% rename from qos-service-impl/src/test/resources/qos.yml rename to qos-service-impl/src/test/resources/qos-server.yml diff --git a/qos-service-impl/versions.lock b/qos-service-impl/versions.lock index 664dc4edbce..a419e2f686b 100644 --- a/qos-service-impl/versions.lock +++ b/qos-service-impl/versions.lock @@ -3,7 +3,10 @@ "com.fasterxml.jackson.core:jackson-annotations": { "locked": "2.6.7", "transitive": [ - "com.fasterxml.jackson.core:jackson-databind" + "com.fasterxml.jackson.core:jackson-databind", + "com.palantir.atlasdb:atlasdb-api", + "com.palantir.atlasdb:atlasdb-commons", + "com.palantir.atlasdb:timestamp-api" ] }, "com.fasterxml.jackson.core:jackson-core": { @@ -14,7 +17,8 @@ "com.fasterxml.jackson.datatype:jackson-datatype-guava", "com.fasterxml.jackson.datatype:jackson-datatype-jdk8", "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", - "com.fasterxml.jackson.module:jackson-module-afterburner" + "com.fasterxml.jackson.module:jackson-module-afterburner", + "com.palantir.atlasdb:atlasdb-client" ] }, "com.fasterxml.jackson.core:jackson-databind": { @@ -25,6 +29,8 @@ "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", "com.fasterxml.jackson.module:jackson-module-afterburner", "com.netflix.feign:feign-jackson", + "com.palantir.atlasdb:atlasdb-api", + "com.palantir.atlasdb:atlasdb-client", "com.palantir.atlasdb:qos-service-api", "com.palantir.remoting-api:errors", "com.palantir.remoting-api:ssl-config", @@ -44,6 +50,7 @@ "com.fasterxml.jackson.datatype:jackson-datatype-guava": { "locked": "2.6.7", "transitive": [ + "com.palantir.atlasdb:atlasdb-client", "com.palantir.remoting3:jackson-support", "com.palantir.remoting3:tracing" ] @@ -72,11 +79,32 @@ "com.google.code.findbugs:annotations": { "locked": "2.0.3", "transitive": [ - "com.palantir.atlasdb:qos-service-api" + "com.palantir.atlasdb:atlasdb-api", + "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-client-protobufs", + "com.palantir.atlasdb:atlasdb-commons", + "com.palantir.atlasdb:commons-executors", + "com.palantir.atlasdb:qos-service-api", + "com.palantir.atlasdb:timestamp-api", + "com.palantir.tritium:tritium-api", + "com.palantir.tritium:tritium-core", + "com.palantir.tritium:tritium-lib", + "com.palantir.tritium:tritium-metrics", + "com.palantir.tritium:tritium-slf4j", + "com.palantir.tritium:tritium-tracing" + ] + }, + "com.google.code.findbugs:jsr305": { + "locked": "3.0.1", + "transitive": [ + "com.palantir.atlasdb:atlasdb-commons", + "com.palantir.remoting-api:errors", + "com.palantir.remoting3:jaxrs-clients", + "com.palantir.remoting3:refresh-utils" ] }, "com.google.guava:guava": { - "locked": "21.0", + "locked": "18.0", "transitive": [ "com.fasterxml.jackson.datatype:jackson-datatype-guava", "com.palantir.remoting3:error-handling", @@ -87,6 +115,25 @@ "com.palantir.remoting3:tracing" ] }, + "com.google.protobuf:protobuf-java": { + "locked": "2.6.0", + "transitive": [ + "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-client-protobufs" + ] + }, + "com.googlecode.json-simple:json-simple": { + "locked": "1.1.1", + "transitive": [ + "com.palantir.atlasdb:atlasdb-client" + ] + }, + "com.googlecode.protobuf-java-format:protobuf-java-format": { + "locked": "1.2", + "transitive": [ + "com.palantir.atlasdb:atlasdb-client" + ] + }, "com.netflix.feign:feign-core": { "locked": "8.17.0", "transitive": [ @@ -120,9 +167,47 @@ "com.palantir.remoting3:jaxrs-clients" ] }, - "com.palantir.atlasdb:qos-service-api": { + "com.palantir.atlasdb:atlasdb-api": { + "project": true, + "transitive": [ + "com.palantir.atlasdb:atlasdb-client" + ] + }, + "com.palantir.atlasdb:atlasdb-client": { "project": true }, + "com.palantir.atlasdb:atlasdb-client-protobufs": { + "project": true, + "transitive": [ + "com.palantir.atlasdb:atlasdb-client" + ] + }, + "com.palantir.atlasdb:atlasdb-commons": { + "project": true, + "transitive": [ + "com.palantir.atlasdb:atlasdb-api", + "com.palantir.atlasdb:atlasdb-client" + ] + }, + "com.palantir.atlasdb:commons-executors": { + "project": true, + "transitive": [ + "com.palantir.atlasdb:atlasdb-commons" + ] + }, + "com.palantir.atlasdb:qos-service-api": { + "project": true, + "transitive": [ + "com.palantir.atlasdb:atlasdb-api", + "com.palantir.atlasdb:atlasdb-client" + ] + }, + "com.palantir.atlasdb:timestamp-api": { + "project": true, + "transitive": [ + "com.palantir.atlasdb:atlasdb-api" + ] + }, "com.palantir.remoting-api:errors": { "locked": "1.4.0", "transitive": [ @@ -138,6 +223,7 @@ "com.palantir.remoting-api:ssl-config": { "locked": "1.4.0", "transitive": [ + "com.palantir.atlasdb:atlasdb-api", "com.palantir.remoting-api:service-config", "com.palantir.remoting3:keystores" ] @@ -173,6 +259,7 @@ "com.palantir.remoting3:jaxrs-clients": { "locked": "3.5.1", "transitive": [ + "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:qos-service-api" ] }, @@ -198,6 +285,7 @@ "com.palantir.remoting3:tracing": { "locked": "3.5.1", "transitive": [ + "com.palantir.atlasdb:atlasdb-client", "com.palantir.remoting3:tracing-okhttp3" ] }, @@ -211,9 +299,17 @@ "com.palantir.safe-logging:safe-logging": { "locked": "0.1.3", "transitive": [ + "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-commons", "com.palantir.atlasdb:qos-service-api", + "com.palantir.atlasdb:timestamp-api", "com.palantir.remoting-api:errors", - "com.palantir.remoting3:tracing" + "com.palantir.remoting3:tracing", + "com.palantir.tritium:tritium-core", + "com.palantir.tritium:tritium-lib", + "com.palantir.tritium:tritium-metrics", + "com.palantir.tritium:tritium-slf4j", + "com.palantir.tritium:tritium-tracing" ] }, "com.palantir.tokens:auth-tokens": { @@ -222,51 +318,161 @@ "com.palantir.remoting-api:service-config" ] }, + "com.palantir.tritium:tritium-api": { + "locked": "0.8.4", + "transitive": [ + "com.palantir.tritium:tritium-core", + "com.palantir.tritium:tritium-lib", + "com.palantir.tritium:tritium-metrics", + "com.palantir.tritium:tritium-slf4j", + "com.palantir.tritium:tritium-tracing" + ] + }, + "com.palantir.tritium:tritium-core": { + "locked": "0.8.4", + "transitive": [ + "com.palantir.tritium:tritium-lib", + "com.palantir.tritium:tritium-metrics", + "com.palantir.tritium:tritium-slf4j", + "com.palantir.tritium:tritium-tracing" + ] + }, + "com.palantir.tritium:tritium-lib": { + "locked": "0.8.4", + "transitive": [ + "com.palantir.atlasdb:atlasdb-client" + ] + }, + "com.palantir.tritium:tritium-metrics": { + "locked": "0.8.4", + "transitive": [ + "com.palantir.tritium:tritium-lib" + ] + }, + "com.palantir.tritium:tritium-proxy": { + "locked": "0.8.4", + "transitive": [ + "com.palantir.tritium:tritium-lib" + ] + }, + "com.palantir.tritium:tritium-slf4j": { + "locked": "0.8.4", + "transitive": [ + "com.palantir.tritium:tritium-lib" + ] + }, + "com.palantir.tritium:tritium-tracing": { + "locked": "0.8.4", + "transitive": [ + "com.palantir.tritium:tritium-lib" + ] + }, "com.squareup.okhttp3:logging-interceptor": { "locked": "3.8.1", "transitive": [ "com.palantir.remoting3:okhttp-clients" ] }, + "com.squareup:javapoet": { + "locked": "1.9.0", + "transitive": [ + "com.palantir.atlasdb:atlasdb-client" + ] + }, + "commons-lang:commons-lang": { + "locked": "2.6", + "transitive": [ + "com.palantir.atlasdb:atlasdb-client" + ] + }, "io.dropwizard.metrics:metrics-core": { "locked": "3.2.3", "transitive": [ + "com.palantir.atlasdb:atlasdb-commons", "com.palantir.remoting3:okhttp-clients" ] }, + "javax.validation:validation-api": { + "locked": "1.1.0.Final", + "transitive": [ + "com.palantir.atlasdb:atlasdb-api" + ] + }, "javax.ws.rs:javax.ws.rs-api": { "locked": "2.0.1", "transitive": [ + "com.palantir.atlasdb:atlasdb-api", + "com.palantir.atlasdb:atlasdb-commons", "com.palantir.atlasdb:qos-service-api", + "com.palantir.atlasdb:timestamp-api", "com.palantir.remoting-api:errors", "com.palantir.remoting3:error-handling", "com.palantir.remoting3:jaxrs-clients" ] }, + "net.jpountz.lz4:lz4": { + "locked": "1.3.0", + "transitive": [ + "com.palantir.atlasdb:atlasdb-commons" + ] + }, + "org.apache.commons:commons-lang3": { + "locked": "3.1", + "transitive": [ + "com.palantir.atlasdb:atlasdb-api" + ] + }, + "org.hdrhistogram:HdrHistogram": { + "locked": "2.1.10", + "transitive": [ + "com.palantir.atlasdb:atlasdb-client" + ] + }, "org.jvnet:animal-sniffer-annotation": { "locked": "1.0", "transitive": [ "com.netflix.feign:feign-core" ] }, + "org.mpierce.metrics.reservoir:hdrhistogram-metrics-reservoir": { + "locked": "1.1.2", + "transitive": [ + "com.palantir.tritium:tritium-metrics" + ] + }, "org.slf4j:slf4j-api": { "locked": "1.7.25", "transitive": [ "com.netflix.feign:feign-slf4j", + "com.palantir.atlasdb:atlasdb-commons", "com.palantir.remoting3:error-handling", "com.palantir.remoting3:jaxrs-clients", "com.palantir.remoting3:okhttp-clients", "com.palantir.remoting3:tracing", "com.palantir.tokens:auth-tokens", + "com.palantir.tritium:tritium-core", + "com.palantir.tritium:tritium-lib", + "com.palantir.tritium:tritium-metrics", + "com.palantir.tritium:tritium-slf4j", + "com.palantir.tritium:tritium-tracing", "io.dropwizard.metrics:metrics-core" ] + }, + "org.xerial.snappy:snappy-java": { + "locked": "1.1.1.7", + "transitive": [ + "com.palantir.atlasdb:atlasdb-client" + ] } }, "runtime": { "com.fasterxml.jackson.core:jackson-annotations": { "locked": "2.6.7", "transitive": [ - "com.fasterxml.jackson.core:jackson-databind" + "com.fasterxml.jackson.core:jackson-databind", + "com.palantir.atlasdb:atlasdb-api", + "com.palantir.atlasdb:atlasdb-commons", + "com.palantir.atlasdb:timestamp-api" ] }, "com.fasterxml.jackson.core:jackson-core": { @@ -277,7 +483,8 @@ "com.fasterxml.jackson.datatype:jackson-datatype-guava", "com.fasterxml.jackson.datatype:jackson-datatype-jdk8", "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", - "com.fasterxml.jackson.module:jackson-module-afterburner" + "com.fasterxml.jackson.module:jackson-module-afterburner", + "com.palantir.atlasdb:atlasdb-client" ] }, "com.fasterxml.jackson.core:jackson-databind": { @@ -288,6 +495,8 @@ "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", "com.fasterxml.jackson.module:jackson-module-afterburner", "com.netflix.feign:feign-jackson", + "com.palantir.atlasdb:atlasdb-api", + "com.palantir.atlasdb:atlasdb-client", "com.palantir.atlasdb:qos-service-api", "com.palantir.remoting-api:errors", "com.palantir.remoting-api:ssl-config", @@ -307,6 +516,7 @@ "com.fasterxml.jackson.datatype:jackson-datatype-guava": { "locked": "2.6.7", "transitive": [ + "com.palantir.atlasdb:atlasdb-client", "com.palantir.remoting3:jackson-support", "com.palantir.remoting3:tracing" ] @@ -335,11 +545,32 @@ "com.google.code.findbugs:annotations": { "locked": "2.0.3", "transitive": [ - "com.palantir.atlasdb:qos-service-api" + "com.palantir.atlasdb:atlasdb-api", + "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-client-protobufs", + "com.palantir.atlasdb:atlasdb-commons", + "com.palantir.atlasdb:commons-executors", + "com.palantir.atlasdb:qos-service-api", + "com.palantir.atlasdb:timestamp-api", + "com.palantir.tritium:tritium-api", + "com.palantir.tritium:tritium-core", + "com.palantir.tritium:tritium-lib", + "com.palantir.tritium:tritium-metrics", + "com.palantir.tritium:tritium-slf4j", + "com.palantir.tritium:tritium-tracing" + ] + }, + "com.google.code.findbugs:jsr305": { + "locked": "3.0.1", + "transitive": [ + "com.palantir.atlasdb:atlasdb-commons", + "com.palantir.remoting-api:errors", + "com.palantir.remoting3:jaxrs-clients", + "com.palantir.remoting3:refresh-utils" ] }, "com.google.guava:guava": { - "locked": "21.0", + "locked": "18.0", "transitive": [ "com.fasterxml.jackson.datatype:jackson-datatype-guava", "com.palantir.remoting3:error-handling", @@ -350,6 +581,25 @@ "com.palantir.remoting3:tracing" ] }, + "com.google.protobuf:protobuf-java": { + "locked": "2.6.0", + "transitive": [ + "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-client-protobufs" + ] + }, + "com.googlecode.json-simple:json-simple": { + "locked": "1.1.1", + "transitive": [ + "com.palantir.atlasdb:atlasdb-client" + ] + }, + "com.googlecode.protobuf-java-format:protobuf-java-format": { + "locked": "1.2", + "transitive": [ + "com.palantir.atlasdb:atlasdb-client" + ] + }, "com.netflix.feign:feign-core": { "locked": "8.17.0", "transitive": [ @@ -383,9 +633,47 @@ "com.palantir.remoting3:jaxrs-clients" ] }, - "com.palantir.atlasdb:qos-service-api": { + "com.palantir.atlasdb:atlasdb-api": { + "project": true, + "transitive": [ + "com.palantir.atlasdb:atlasdb-client" + ] + }, + "com.palantir.atlasdb:atlasdb-client": { "project": true }, + "com.palantir.atlasdb:atlasdb-client-protobufs": { + "project": true, + "transitive": [ + "com.palantir.atlasdb:atlasdb-client" + ] + }, + "com.palantir.atlasdb:atlasdb-commons": { + "project": true, + "transitive": [ + "com.palantir.atlasdb:atlasdb-api", + "com.palantir.atlasdb:atlasdb-client" + ] + }, + "com.palantir.atlasdb:commons-executors": { + "project": true, + "transitive": [ + "com.palantir.atlasdb:atlasdb-commons" + ] + }, + "com.palantir.atlasdb:qos-service-api": { + "project": true, + "transitive": [ + "com.palantir.atlasdb:atlasdb-api", + "com.palantir.atlasdb:atlasdb-client" + ] + }, + "com.palantir.atlasdb:timestamp-api": { + "project": true, + "transitive": [ + "com.palantir.atlasdb:atlasdb-api" + ] + }, "com.palantir.remoting-api:errors": { "locked": "1.4.0", "transitive": [ @@ -401,6 +689,7 @@ "com.palantir.remoting-api:ssl-config": { "locked": "1.4.0", "transitive": [ + "com.palantir.atlasdb:atlasdb-api", "com.palantir.remoting-api:service-config", "com.palantir.remoting3:keystores" ] @@ -436,6 +725,7 @@ "com.palantir.remoting3:jaxrs-clients": { "locked": "3.5.1", "transitive": [ + "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:qos-service-api" ] }, @@ -461,6 +751,7 @@ "com.palantir.remoting3:tracing": { "locked": "3.5.1", "transitive": [ + "com.palantir.atlasdb:atlasdb-client", "com.palantir.remoting3:tracing-okhttp3" ] }, @@ -474,9 +765,17 @@ "com.palantir.safe-logging:safe-logging": { "locked": "0.1.3", "transitive": [ + "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-commons", "com.palantir.atlasdb:qos-service-api", + "com.palantir.atlasdb:timestamp-api", "com.palantir.remoting-api:errors", - "com.palantir.remoting3:tracing" + "com.palantir.remoting3:tracing", + "com.palantir.tritium:tritium-core", + "com.palantir.tritium:tritium-lib", + "com.palantir.tritium:tritium-metrics", + "com.palantir.tritium:tritium-slf4j", + "com.palantir.tritium:tritium-tracing" ] }, "com.palantir.tokens:auth-tokens": { @@ -485,44 +784,151 @@ "com.palantir.remoting-api:service-config" ] }, + "com.palantir.tritium:tritium-api": { + "locked": "0.8.4", + "transitive": [ + "com.palantir.tritium:tritium-core", + "com.palantir.tritium:tritium-lib", + "com.palantir.tritium:tritium-metrics", + "com.palantir.tritium:tritium-slf4j", + "com.palantir.tritium:tritium-tracing" + ] + }, + "com.palantir.tritium:tritium-core": { + "locked": "0.8.4", + "transitive": [ + "com.palantir.tritium:tritium-lib", + "com.palantir.tritium:tritium-metrics", + "com.palantir.tritium:tritium-slf4j", + "com.palantir.tritium:tritium-tracing" + ] + }, + "com.palantir.tritium:tritium-lib": { + "locked": "0.8.4", + "transitive": [ + "com.palantir.atlasdb:atlasdb-client" + ] + }, + "com.palantir.tritium:tritium-metrics": { + "locked": "0.8.4", + "transitive": [ + "com.palantir.tritium:tritium-lib" + ] + }, + "com.palantir.tritium:tritium-proxy": { + "locked": "0.8.4", + "transitive": [ + "com.palantir.tritium:tritium-lib" + ] + }, + "com.palantir.tritium:tritium-slf4j": { + "locked": "0.8.4", + "transitive": [ + "com.palantir.tritium:tritium-lib" + ] + }, + "com.palantir.tritium:tritium-tracing": { + "locked": "0.8.4", + "transitive": [ + "com.palantir.tritium:tritium-lib" + ] + }, "com.squareup.okhttp3:logging-interceptor": { "locked": "3.8.1", "transitive": [ "com.palantir.remoting3:okhttp-clients" ] }, + "com.squareup:javapoet": { + "locked": "1.9.0", + "transitive": [ + "com.palantir.atlasdb:atlasdb-client" + ] + }, + "commons-lang:commons-lang": { + "locked": "2.6", + "transitive": [ + "com.palantir.atlasdb:atlasdb-client" + ] + }, "io.dropwizard.metrics:metrics-core": { "locked": "3.2.3", "transitive": [ + "com.palantir.atlasdb:atlasdb-commons", "com.palantir.remoting3:okhttp-clients" ] }, + "javax.validation:validation-api": { + "locked": "1.1.0.Final", + "transitive": [ + "com.palantir.atlasdb:atlasdb-api" + ] + }, "javax.ws.rs:javax.ws.rs-api": { "locked": "2.0.1", "transitive": [ + "com.palantir.atlasdb:atlasdb-api", + "com.palantir.atlasdb:atlasdb-commons", "com.palantir.atlasdb:qos-service-api", + "com.palantir.atlasdb:timestamp-api", "com.palantir.remoting-api:errors", "com.palantir.remoting3:error-handling", "com.palantir.remoting3:jaxrs-clients" ] }, + "net.jpountz.lz4:lz4": { + "locked": "1.3.0", + "transitive": [ + "com.palantir.atlasdb:atlasdb-commons" + ] + }, + "org.apache.commons:commons-lang3": { + "locked": "3.1", + "transitive": [ + "com.palantir.atlasdb:atlasdb-api" + ] + }, + "org.hdrhistogram:HdrHistogram": { + "locked": "2.1.10", + "transitive": [ + "com.palantir.atlasdb:atlasdb-client" + ] + }, "org.jvnet:animal-sniffer-annotation": { "locked": "1.0", "transitive": [ "com.netflix.feign:feign-core" ] }, + "org.mpierce.metrics.reservoir:hdrhistogram-metrics-reservoir": { + "locked": "1.1.2", + "transitive": [ + "com.palantir.tritium:tritium-metrics" + ] + }, "org.slf4j:slf4j-api": { "locked": "1.7.25", "transitive": [ "com.netflix.feign:feign-slf4j", + "com.palantir.atlasdb:atlasdb-commons", "com.palantir.remoting3:error-handling", "com.palantir.remoting3:jaxrs-clients", "com.palantir.remoting3:okhttp-clients", "com.palantir.remoting3:tracing", "com.palantir.tokens:auth-tokens", + "com.palantir.tritium:tritium-core", + "com.palantir.tritium:tritium-lib", + "com.palantir.tritium:tritium-metrics", + "com.palantir.tritium:tritium-slf4j", + "com.palantir.tritium:tritium-tracing", "io.dropwizard.metrics:metrics-core" ] + }, + "org.xerial.snappy:snappy-java": { + "locked": "1.1.1.7", + "transitive": [ + "com.palantir.atlasdb:atlasdb-client" + ] } } } \ No newline at end of file diff --git a/timelock-agent/versions.lock b/timelock-agent/versions.lock index d4e51f093f8..3569184ba52 100644 --- a/timelock-agent/versions.lock +++ b/timelock-agent/versions.lock @@ -271,6 +271,7 @@ "project": true, "transitive": [ "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl", "com.palantir.atlasdb:timestamp-impl" ] }, @@ -370,6 +371,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, @@ -1050,6 +1052,7 @@ "project": true, "transitive": [ "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl", "com.palantir.atlasdb:timestamp-impl" ] }, @@ -1149,6 +1152,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, diff --git a/timelock-impl/versions.lock b/timelock-impl/versions.lock index 6c4f07be161..1beed851893 100644 --- a/timelock-impl/versions.lock +++ b/timelock-impl/versions.lock @@ -270,6 +270,7 @@ "project": true, "transitive": [ "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl", "com.palantir.atlasdb:timestamp-impl" ] }, @@ -364,6 +365,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, @@ -1035,6 +1037,7 @@ "project": true, "transitive": [ "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl", "com.palantir.atlasdb:timestamp-impl" ] }, @@ -1129,6 +1132,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, diff --git a/timelock-server-distribution/versions.lock b/timelock-server-distribution/versions.lock index 924287da028..51b8b1c38a9 100644 --- a/timelock-server-distribution/versions.lock +++ b/timelock-server-distribution/versions.lock @@ -405,6 +405,7 @@ "com.palantir.atlasdb:atlasdb-dbkvs", "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:atlasdb-jdbc", + "com.palantir.atlasdb:qos-service-impl", "com.palantir.atlasdb:timestamp-impl" ] }, @@ -564,6 +565,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, diff --git a/timelock-server/versions.lock b/timelock-server/versions.lock index 477cfc0f188..a547481f3bd 100644 --- a/timelock-server/versions.lock +++ b/timelock-server/versions.lock @@ -332,6 +332,7 @@ "project": true, "transitive": [ "com.palantir.atlasdb:atlasdb-impl-shared", + "com.palantir.atlasdb:qos-service-impl", "com.palantir.atlasdb:timestamp-impl" ] }, @@ -431,6 +432,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] }, @@ -1718,6 +1720,7 @@ "com.palantir.atlasdb:atlasdb-dbkvs", "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:atlasdb-jdbc", + "com.palantir.atlasdb:qos-service-impl", "com.palantir.atlasdb:timestamp-impl" ] }, @@ -1870,6 +1873,7 @@ "transitive": [ "com.palantir.atlasdb:atlasdb-api", "com.palantir.atlasdb:atlasdb-client", + "com.palantir.atlasdb:atlasdb-impl-shared", "com.palantir.atlasdb:qos-service-impl" ] },