/**
* Copyright 2017 LinkedIn Corp. All rights reserved.
*
* 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.
*/
package com.github.ambry.store;
import com.github.ambry.server.StatsSnapshot;
import com.github.ambry.utils.Pair;
import com.github.ambry.utils.Time;
import com.github.ambry.utils.Utils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* Exposes stats related to a {@link BlobStore} that is useful to different components.
*
* Note: This is the v0 implementation of BlobStoreStats. The v0 implementation walks through the entire index
* and collect data needed to serve stats related requests.
*/
class BlobStoreStats implements StoreStats {
static final String IO_SCHEDULER_JOB_TYPE = "BlobStoreStats";
static final String IO_SCHEDULER_JOB_ID = "indexSegment_read";
// Max blob size thats encountered while generating stats
// TODO: make this dynamically generated
private static final long MAX_BLOB_SIZE = 4 * 1024 * 1024;
private final PersistentIndex index;
private final Time time;
private final DiskIOScheduler diskIOScheduler;
/**
* Convert a given nested {@link Map} of accountId to containerId to valid size to its corresponding
* {@link StatsSnapshot} object.
* @param quotaMap the nested {@link Map} to be converted
* @return the corresponding {@link StatsSnapshot} object.
*/
static StatsSnapshot convertQuotaToStatsSnapshot(Map<String, Map<String, Long>> quotaMap) {
Map<String, StatsSnapshot> accountValidSizeMap = new HashMap<>();
long totalSize = 0;
for (Map.Entry<String, Map<String, Long>> accountEntry : quotaMap.entrySet()) {
long subTotalSize = 0;
Map<String, StatsSnapshot> containerValidSizeMap = new HashMap<>();
for (Map.Entry<String, Long> containerEntry : accountEntry.getValue().entrySet()) {
subTotalSize += containerEntry.getValue();
containerValidSizeMap.put(containerEntry.getKey(), new StatsSnapshot(containerEntry.getValue(), null));
}
totalSize += subTotalSize;
accountValidSizeMap.put(accountEntry.getKey(), new StatsSnapshot(subTotalSize, containerValidSizeMap));
}
return new StatsSnapshot(totalSize, accountValidSizeMap);
}
BlobStoreStats(PersistentIndex index, Time time, DiskIOScheduler diskIOScheduler) {
this.index = index;
this.time = time;
this.diskIOScheduler = diskIOScheduler;
}
@Override
public Pair<Long, Long> getValidSize(TimeRange timeRange) throws StoreException {
Pair<Long, NavigableMap<String, Long>> logSegmentValidSizeResult = getValidDataSizeByLogSegment(timeRange);
Long totalValidSize = 0L;
for (Long value : logSegmentValidSizeResult.getSecond().values()) {
totalValidSize += value;
}
return new Pair<>(logSegmentValidSizeResult.getFirst(), totalValidSize);
}
/**
* {@inheritDoc}
* The implementation in {@link BlobStoreStats} returns quota related stats of a {@link BlobStore}.
*/
@Override
public StatsSnapshot getStatsSnapshot() throws StoreException {
return convertQuotaToStatsSnapshot(getValidDataSizeByContainer());
}
/**
* Returns the max blob size that is encountered while generating stats
* @return the max blob size that is encountered while generating stats
*/
long getMaxBlobSize() {
return MAX_BLOB_SIZE;
}
/**
* Gets the size of valid data at a particular point in time for all log segments. The caller specifies a reference
* time and acceptable resolution for the stats in the form of a {@link TimeRange}. The store will return data
* for a point in time within the specified range.
* The following data are considered as valid data for this API:
* 1. PUT with no expiry and no corresponding DELETE
* 2. PUT expiring at t_exp but t_exp_ref < t_exp
* 3. PUT with corresponding DELETE at time t_delete but t_del_ref < t_delete
* 4. DELETE record
* For this API, t_del_ref is based on the given {@link TimeRange} and t_exp_ref is the time when the API is called.
* @param timeRange the reference {@link TimeRange} at which the data is requested. Defines both the reference time
* and the acceptable resolution.
* @return a {@link Pair} whose first element is the time at which stats was collected (in ms) and whose second
* element is the valid data size for each segment in the form of a {@link NavigableMap} of segment names to
* valid data sizes.
*/
Pair<Long, NavigableMap<String, Long>> getValidDataSizeByLogSegment(TimeRange timeRange) throws StoreException {
long deleteReferenceTimeInMs = timeRange.getEndTimeInMs();
long expirationReferenceTimeInMs = time.milliseconds();
NavigableMap<String, Long> validSizePerLogSegment =
collectValidDataSizeByLogSegment(deleteReferenceTimeInMs, expirationReferenceTimeInMs);
return new Pair<>(deleteReferenceTimeInMs, validSizePerLogSegment);
}
/**
* Gets the size of valid data for all serviceIds and their containerIds as of now (the time when the API is called).
* The following data are considered as valid data for this API:
* 1. PUT with no expiry and no corresponding DELETE
* 2. PUT expiring at t_exp but t_exp_ref < t_exp
* 3. PUT with corresponding DELETE at time t_delete but t_del_ref < t_delete
* For this API, t_del_ref and t_exp_ref are the same and its value is when the API is called.
* @return the valid data size for each container in the form of a nested {@link Map} of serviceIds to another map of
* containerIds to valid data size.
*/
Map<String, Map<String, Long>> getValidDataSizeByContainer() throws StoreException {
long deleteAndExpirationRefTimeInMs = time.milliseconds();
return collectValidDataSizeByContainer(deleteAndExpirationRefTimeInMs);
}
/**
* Walk through the entire index and collect valid data size information per container (delete records not included).
* @param deleteAndExpirationRefTimeInMs the reference time in ms until which deletes and expiration are relevant
* @return a nested {@link Map} of serviceId to containerId to valid data size
*/
private Map<String, Map<String, Long>> collectValidDataSizeByContainer(long deleteAndExpirationRefTimeInMs)
throws StoreException {
Set<StoreKey> deletedKeys = new HashSet<>();
Map<String, Map<String, Long>> validDataSizePerContainer = new HashMap<>();
for (IndexSegment indexSegment : index.getIndexSegments().descendingMap().values()) {
List<IndexValue> validIndexValues =
getValidIndexValues(indexSegment, deleteAndExpirationRefTimeInMs, deleteAndExpirationRefTimeInMs,
deletedKeys);
for (IndexValue value : validIndexValues) {
if (!value.isFlagSet(IndexValue.Flags.Delete_Index)) {
// delete records does not count towards valid data size for quota (containers)
updateNestedMapHelper(validDataSizePerContainer, String.valueOf(value.getServiceId()),
String.valueOf(value.getContainerId()), value.getSize());
}
}
}
return validDataSizePerContainer;
}
/**
* Walk through the entire index and collect valid size information per log segment (size of delete records included).
* @param deleteReferenceTimeInMs the reference time in ms until which deletes are relevant
* @param expirationReferenceTimeInMs the reference time in ms until which expiration are relevant
* @return a {@link NavigableMap} of log segment name to valid data size
*/
private NavigableMap<String, Long> collectValidDataSizeByLogSegment(long deleteReferenceTimeInMs,
long expirationReferenceTimeInMs) throws StoreException {
Set<StoreKey> deletedKeys = new HashSet<>();
NavigableMap<String, Long> validSizePerLogSegment = new TreeMap<>();
for (IndexSegment indexSegment : index.getIndexSegments().descendingMap().values()) {
List<IndexValue> validIndexValues =
getValidIndexValues(indexSegment, deleteReferenceTimeInMs, expirationReferenceTimeInMs, deletedKeys);
for (IndexValue value : validIndexValues) {
updateMapHelper(validSizePerLogSegment, indexSegment.getLogSegmentName(), value.getSize());
}
}
return validSizePerLogSegment;
}
/**
* Get a {@link List} of valid {@link IndexValue} in a given {@link IndexSegment}.
* @param indexSegment the indexSegment to be read from
* @param deleteReferenceTimeInMs the reference time in ms until which deletes are relevant
* @param expirationReferenceTimeInMs the reference time in ms until which expiration are relevant
* @param deletedKeys a {@link Set} of deleted keys to help determine whether a put is deleted
* @return a {@link List} of valid {@link IndexValue}
* @throws StoreException
*/
private List<IndexValue> getValidIndexValues(IndexSegment indexSegment, long deleteReferenceTimeInMs,
long expirationReferenceTimeInMs, Set<StoreKey> deletedKeys) throws StoreException {
diskIOScheduler.getSlice(BlobStoreStats.IO_SCHEDULER_JOB_TYPE, BlobStoreStats.IO_SCHEDULER_JOB_ID, 1);
List<IndexEntry> indexEntries = new ArrayList<>();
List<IndexValue> validIndexValues = new ArrayList<>();
try {
indexSegment.getIndexEntriesSince(null, new FindEntriesCondition(Integer.MAX_VALUE), indexEntries,
new AtomicLong(0));
} catch (IOException e) {
throw new StoreException("I/O error while getting entries from index segment", e, StoreErrorCodes.IOError);
}
for (IndexEntry indexEntry : indexEntries) {
IndexValue indexValue = indexEntry.getValue();
if (indexValue.isFlagSet(IndexValue.Flags.Delete_Index)) {
// delete record
validIndexValues.add(indexValue);
long operationTimeInMs =
indexValue.getOperationTimeInMs() == Utils.Infinite_Time ? indexSegment.getLastModifiedTimeMs()
: indexValue.getOperationTimeInMs();
if (operationTimeInMs < deleteReferenceTimeInMs) {
// delete is relevant
deletedKeys.add(indexEntry.getKey());
} else if (!isExpired(indexValue.getExpiresAtMs(), expirationReferenceTimeInMs)
&& indexValue.getOriginalMessageOffset() != IndexValue.UNKNOWN_ORIGINAL_MESSAGE_OFFSET
&& indexValue.getOriginalMessageOffset() != indexValue.getOffset().getOffset()
&& indexValue.getOriginalMessageOffset() >= indexSegment.getStartOffset().getOffset()) {
// delete is irrelevant but it's in the same index segment as the put and the put is still valid
BlobReadOptions originalPut =
index.getBlobReadInfo(indexEntry.getKey(), EnumSet.of(StoreGetOptions.Store_Include_Deleted));
Offset originalPutOffset = new Offset(originalPut.getLogSegmentName(), originalPut.getOffset());
validIndexValues.add(new IndexValue(originalPut.getSize(), originalPutOffset, originalPut.getExpiresAtMs(),
Utils.Infinite_Time, indexValue.getServiceId(), indexValue.getContainerId()));
}
} else if (!isExpired(indexValue.getExpiresAtMs(), expirationReferenceTimeInMs) && !deletedKeys.contains(
indexEntry.getKey())) {
// put record that is not deleted and not expired according to deleteReferenceTimeInMs and expirationReferenceTimeInMs
validIndexValues.add(indexValue);
}
}
return validIndexValues;
}
/**
* Determine whether a blob is expired or not given the expiration time and a reference time.
* @param expirationTimeInMs the expiration time of the blob in ms
* @param referenceTimeInMs the reference time in ms until which expiration are relevant
* @return whether the blob is expired or not
*/
private boolean isExpired(long expirationTimeInMs, long referenceTimeInMs) {
return expirationTimeInMs != Utils.Infinite_Time && expirationTimeInMs < referenceTimeInMs;
}
/**
* Helper function to update nested map data structure.
* @param nestedMap nested {@link Map} to be updated
* @param firstKey of the nested map
* @param secondKey of the nested map
* @param value the value to be added at the corresponding entry
*/
private void updateNestedMapHelper(Map<String, Map<String, Long>> nestedMap, String firstKey, String secondKey,
Long value) {
if (!nestedMap.containsKey(firstKey)) {
nestedMap.put(firstKey, new HashMap<String, Long>());
}
updateMapHelper(nestedMap.get(firstKey), secondKey, value);
}
/**
* Helper function to update map data structure.
* @param map {@link Map} to be updated
* @param key of the map
* @param value the value to be added at the corresponding entry
*/
private void updateMapHelper(Map<String, Long> map, String key, Long value) {
Long newValue = map.containsKey(key) ? map.get(key) + value : value;
map.put(key, newValue);
}
}