/*
* Copyright 2008-2014 LinkedIn, Inc
*
* 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 voldemort.store.quota;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
import voldemort.VoldemortException;
import voldemort.store.DelegatingStore;
import voldemort.store.Store;
import voldemort.store.configuration.FileBackedCachingStorageEngine;
import voldemort.store.metadata.MetadataStore;
import voldemort.store.stats.StoreStats;
import voldemort.store.stats.Tracked;
import voldemort.utils.ByteArray;
import voldemort.utils.Utils;
import voldemort.versioning.Version;
import voldemort.versioning.Versioned;
public class QuotaLimitingStore extends DelegatingStore<ByteArray, byte[], byte[]> {
private static final Logger logger = Logger.getLogger(QuotaLimitingStore.class.getName());
private final StoreStats stats;
private final QuotaLimitStats quotaStats;
private final FileBackedCachingStorageEngine quotaStore;
private final MetadataStore metadataStore;
private final String getQuotaKey;
private final String putQuotaKey;
public QuotaLimitingStore(Store<ByteArray, byte[], byte[]> innerStore,
StoreStats stats,
QuotaLimitStats quotaStats,
FileBackedCachingStorageEngine quotaStore,
MetadataStore metadataStore) {
super(innerStore);
this.stats = stats;
this.quotaStore = quotaStore;
this.metadataStore = metadataStore;
this.getQuotaKey = QuotaUtils.makeQuotaKey(innerStore.getName(), QuotaType.GET_THROUGHPUT);
this.putQuotaKey = QuotaUtils.makeQuotaKey(innerStore.getName(), QuotaType.PUT_THROUGHPUT);
this.quotaStats = quotaStats;
}
public StoreStats getStats() {
return stats;
}
private float getThroughput(Tracked trackedOp) {
if(trackedOp.equals(Tracked.GET)) {
float getThroughPut = this.stats.getThroughput(Tracked.GET);
float getAllThroughPut = this.stats.getGetAllKeysThroughput();
return getThroughPut + getAllThroughPut;
} else if(trackedOp.equals(Tracked.PUT)) {
float putThroughPut = this.stats.getThroughput(Tracked.PUT);
float deleteThroughPut = this.stats.getThroughput(Tracked.DELETE);
return putThroughPut + deleteThroughPut;
} else {
throw new IllegalArgumentException("Expected GET or PUT, received " + trackedOp);
}
}
/**
* Ensure the current throughput levels for the tracked operation does not
* exceed set quota limits. Throws an exception if exceeded quota.
*
* @param quotaKey
* @param trackedOp
*/
private void checkRateLimit(String quotaKey, Tracked trackedOp) {
String quotaValue = null;
try {
if(!metadataStore.getQuotaEnforcingEnabledUnlocked()) {
return;
}
quotaValue = quotaStore.cacheGet(quotaKey);
// Store may not have any quotas
if(quotaValue == null) {
return;
}
// But, if it does
float currentRate = getThroughput(trackedOp);
float allowedRate = Float.parseFloat(quotaValue);
// TODO the histogram should be reasonably accurate to do all
// these things.. (ghost qps and all)
// Report the current quota usage level
quotaStats.reportQuotaUsed(trackedOp, Utils.safeGetPercentage(currentRate, allowedRate));
// check if we have exceeded rate.
if(currentRate > allowedRate) {
quotaStats.reportRateLimitedOp(trackedOp);
throw new QuotaExceededException("Exceeded rate limit for " + quotaKey
+ ". Maximum allowed : " + allowedRate
+ " Current: " + currentRate);
}
} catch(NumberFormatException nfe) {
// move on, if we cannot parse quota value properly
logger.debug("Invalid formatting of quota value for key " + quotaKey + " : "
+ quotaValue);
}
}
@Override
public boolean delete(ByteArray key, Version version) throws VoldemortException {
// We want to have only 2 Quotas read and write, hence the Delete uses
// PUT. It would be easier if we rename PUT, GET to WRITE,READ
// but for backward compatibility we are sticking with old names.
checkRateLimit(putQuotaKey, Tracked.PUT);
return super.delete(key, version);
}
@Override
public List<Versioned<byte[]>> get(ByteArray key, byte[] transforms) throws VoldemortException {
checkRateLimit(getQuotaKey, Tracked.GET);
return super.get(key, transforms);
}
@Override
public Map<ByteArray, List<Versioned<byte[]>>> getAll(Iterable<ByteArray> keys,
Map<ByteArray, byte[]> transforms)
throws VoldemortException {
// We want to have only 2 Quotas read and write, hence the GetAll uses
// GET. It would be easier if we rename PUT, GET to WRITE,READ
// but for backward compatibility we are sticking with old names.
checkRateLimit(getQuotaKey, Tracked.GET);
return super.getAll(keys, transforms);
}
@Override
public void put(ByteArray key, Versioned<byte[]> value, byte[] transforms)
throws VoldemortException {
checkRateLimit(putQuotaKey, Tracked.PUT);
super.put(key, value, transforms);
}
}