/* * Copyright 2014-2018 Amazon.com, Inc. or its affiliates. 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. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file 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.amazon.janusgraph.diskstorage.dynamodb; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.janusgraph.diskstorage.BackendException; import org.janusgraph.diskstorage.Entry; import org.janusgraph.diskstorage.EntryList; import org.janusgraph.diskstorage.StaticBuffer; import org.janusgraph.diskstorage.keycolumnvalue.KCVMutation; import org.janusgraph.diskstorage.keycolumnvalue.KeyIterator; import org.janusgraph.diskstorage.keycolumnvalue.KeyRangeQuery; import org.janusgraph.diskstorage.keycolumnvalue.KeySliceQuery; import org.janusgraph.diskstorage.keycolumnvalue.SliceQuery; import org.janusgraph.diskstorage.keycolumnvalue.StoreTransaction; import org.janusgraph.diskstorage.util.StaticArrayEntryList; import com.amazon.janusgraph.diskstorage.dynamodb.builder.ConditionExpressionBuilder; import com.amazon.janusgraph.diskstorage.dynamodb.builder.EntryBuilder; import com.amazon.janusgraph.diskstorage.dynamodb.builder.FilterExpressionBuilder; import com.amazon.janusgraph.diskstorage.dynamodb.builder.ItemBuilder; import com.amazon.janusgraph.diskstorage.dynamodb.builder.MultiUpdateExpressionBuilder; import com.amazon.janusgraph.diskstorage.dynamodb.iterator.MultiRowParallelScanInterpreter; import com.amazon.janusgraph.diskstorage.dynamodb.iterator.MultiRowSequentialScanInterpreter; import com.amazon.janusgraph.diskstorage.dynamodb.iterator.ScanBackedKeyIterator; import com.amazon.janusgraph.diskstorage.dynamodb.iterator.ScanContextInterpreter; import com.amazon.janusgraph.diskstorage.dynamodb.iterator.Scanner; import com.amazon.janusgraph.diskstorage.dynamodb.iterator.SequentialScanner; import com.amazon.janusgraph.diskstorage.dynamodb.mutation.DeleteItemWorker; import com.amazon.janusgraph.diskstorage.dynamodb.mutation.MutateWorker; import com.amazon.janusgraph.diskstorage.dynamodb.mutation.UpdateItemWorker; import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; import com.amazonaws.services.dynamodbv2.model.AttributeValue; import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest; import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; import com.amazonaws.services.dynamodbv2.model.KeyType; import com.amazonaws.services.dynamodbv2.model.QueryRequest; import com.amazonaws.services.dynamodbv2.model.QueryResult; import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType; import com.amazonaws.services.dynamodbv2.model.ScanRequest; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import lombok.extern.slf4j.Slf4j; /** * Acts as if DynamoDB were a Column Oriented Database by using range query when * required. * * See configuration * storage.dynamodb.stores.***table_name***.data-model=MULTI * * KCV Schema - actual table (Hash(S) + Range(S)): * hk(S) | rk(S) | v(B) <-Attribute Names * 0x01 | 0x02 | 0x03 <-Row Values * 0x01 | 0x04 | 0x05 <-Row Values * * @author Matthew Sowders * @author Alexander Patrikalakis * @author Michael Rodaitis * */ @Slf4j public class DynamoDbStore extends AbstractDynamoDbStore { public DynamoDbStore(final DynamoDBStoreManager manager, final String prefix, final String storeName) { super(manager, prefix, storeName); } private EntryList createEntryListFromItems(final List<Map<String, AttributeValue>> items, final SliceQuery sliceQuery) { final List<Entry> entries = new ArrayList<>(items.size()); for (Map<String, AttributeValue> item : items) { final Entry entry = new EntryBuilder(item).slice(sliceQuery.getSliceStart(), sliceQuery.getSliceEnd()) .build(); if (null != entry) { entries.add(entry); } } return StaticArrayEntryList.of(entries); } @Override public KeyIterator getKeys(final KeyRangeQuery query, final StoreTransaction txh) throws BackendException { throw new UnsupportedOperationException("Byteorder is not maintained."); } @Override public KeyIterator getKeys(final SliceQuery query, final StoreTransaction txh) throws BackendException { log.debug("Entering getKeys table:{} query:{} txh:{}", getTableName(), encodeForLog(query), txh); final Expression filterExpression = new FilterExpressionBuilder().rangeKey() .range(query) .build(); final ScanRequest scanRequest = super.createScanRequest() .withFilterExpression(filterExpression.getConditionExpression()) .withExpressionAttributeValues(filterExpression.getAttributeValues()); final Scanner scanner; final ScanContextInterpreter interpreter; if (client.isEnableParallelScan()) { scanner = client.getDelegate().getParallelScanCompletionService(scanRequest); interpreter = new MultiRowParallelScanInterpreter(this, query); } else { scanner = new SequentialScanner(client.getDelegate(), scanRequest); interpreter = new MultiRowSequentialScanInterpreter(this, query); } final KeyIterator result = new ScanBackedKeyIterator(scanner, interpreter); log.debug("Exiting getKeys table:{} query:{} txh:{} returning:{}", getTableName(), encodeForLog(query), txh, result); return result; } private EntryList getKeysRangeQuery(final StaticBuffer hashKey, final SliceQuery query, final StoreTransaction txh) throws BackendException { log.debug("Range query for hashKey:{} txh:{}", encodeKeyForLog(hashKey), txh); final QueryWorker worker = buildQueryWorker(hashKey, query); final QueryResultWrapper result = worker.call(); return createEntryListFromItems(result.getDynamoDBResult().getItems(), query); } public QueryWorker buildQueryWorker(final StaticBuffer hashKey, final SliceQuery query) { final QueryRequest request = createQueryRequest(hashKey, query); // Only enforce a limit when Titan tells us to if (query.hasLimit()) { final int limit = query.getLimit(); request.setLimit(limit); return new QueryWithLimitWorker(client.getDelegate(), request, hashKey, limit); } return new QueryWorker(client.getDelegate(), request, hashKey); } private QueryRequest createQueryRequest(final StaticBuffer hashKey, final SliceQuery rangeQuery) { final Expression keyConditionExpression = new ConditionExpressionBuilder().hashKey(hashKey) .rangeKey(rangeQuery.getSliceStart(), rangeQuery.getSliceEnd()) .build(); return super.createQueryRequest() .withKeyConditionExpression(keyConditionExpression.getConditionExpression()) .withExpressionAttributeValues(keyConditionExpression.getAttributeValues()); } @Override public EntryList getSlice(final KeySliceQuery query, final StoreTransaction txh) throws BackendException { log.debug("Entering getSliceKeySliceQuery table:{} query:{} txh:{}", getTableName(), encodeForLog(query), txh); final EntryList result = getKeysRangeQuery(query.getKey(), query, txh); log.debug("Exiting getSliceKeySliceQuery table:{} query:{} txh:{} returning:{}", getTableName(), encodeForLog(query), txh, result.size()); return result; } @Override public Map<StaticBuffer, EntryList> getSlice(final List<StaticBuffer> keys, final SliceQuery query, final StoreTransaction txh) throws BackendException { log.debug("Entering getSliceMultiSliceQuery table:{} keys:{} query:{} txh:{}", getTableName(), encodeForLog(keys), encodeForLog(query), txh); final Map<StaticBuffer, EntryList> resultMap = Maps.newHashMapWithExpectedSize(keys.size()); final List<QueryWorker> queryWorkers = Lists.newLinkedList(); for (StaticBuffer hashKey : keys) { final QueryWorker queryWorker = buildQueryWorker(hashKey, query); queryWorkers.add(queryWorker); resultMap.put(hashKey, EntryList.EMPTY_LIST); } final List<QueryResultWrapper> results = client.getDelegate().parallelQuery(queryWorkers); for (QueryResultWrapper resultWrapper : results) { final StaticBuffer titanKey = resultWrapper.getTitanKey(); final QueryResult dynamoDBResult = resultWrapper.getDynamoDBResult(); final EntryList entryList = createEntryListFromItems(dynamoDBResult.getItems(), query); resultMap.put(titanKey, entryList); } log.debug("Exiting getSliceMultiSliceQuery table:{} keys:{} query:{} txh:{} returning:{}", getTableName(), encodeForLog(keys), encodeForLog(query), txh, resultMap.size()); return resultMap; } @Override public void mutate(final StaticBuffer key, final List<Entry> additions, final List<StaticBuffer> deletions, final StoreTransaction txh) throws BackendException { log.debug("Entering mutate table:{} keys:{} additions:{} deletions:{} txh:{}", getTableName(), encodeKeyForLog(key), encodeForLog(additions), encodeForLog(deletions), txh); // this method also filters out deletions that are also added super.mutateOneKey(key, new KCVMutation(additions, deletions), txh); log.debug("Exiting mutate table:{} keys:{} additions:{} deletions:{} txh:{} returning:void", getTableName(), encodeKeyForLog(key), encodeForLog(additions), encodeForLog(deletions), txh); } @Override public String toString() { return "DynamoDBKeyColumnValueStore:" + getTableName(); } @Override public Collection<MutateWorker> createMutationWorkers(final Map<StaticBuffer, KCVMutation> mutationMap, final DynamoDbStoreTransaction txh) { final List<MutateWorker> workers = new LinkedList<>(); for (Map.Entry<StaticBuffer, KCVMutation> entry : mutationMap.entrySet()) { final StaticBuffer hashKey = entry.getKey(); final KCVMutation mutation = entry.getValue(); // Filter out deletions that are also added - TODO why use a set? final Set<StaticBuffer> add = mutation.getAdditions().stream() .map(Entry::getColumn).collect(Collectors.toSet()); final List<StaticBuffer> mutableDeletions = mutation.getDeletions().stream() .filter(del -> !add.contains(del)) .collect(Collectors.toList()); if (mutation.hasAdditions()) { workers.addAll(createWorkersForAdditions(hashKey, mutation.getAdditions(), txh)); } if (!mutableDeletions.isEmpty()) { workers.addAll(createWorkersForDeletions(hashKey, mutableDeletions, txh)); } } return workers; } private Collection<MutateWorker> createWorkersForAdditions(final StaticBuffer hashKey, final List<Entry> additions, final DynamoDbStoreTransaction txh) { return additions.stream().map(addition -> { final StaticBuffer rangeKey = addition.getColumn(); final Map<String, AttributeValue> keys = new ItemBuilder().hashKey(hashKey) .rangeKey(rangeKey) .build(); final Expression updateExpression = new MultiUpdateExpressionBuilder(this, txh).hashKey(hashKey) .rangeKey(rangeKey) .value(addition.getValue()) .build(); return super.createUpdateItemRequest() .withUpdateExpression(updateExpression.getUpdateExpression()) .withConditionExpression(updateExpression.getConditionExpression()) .withExpressionAttributeValues(updateExpression.getAttributeValues()) .withKey(keys); }) .map(request -> new UpdateItemWorker(request, client.getDelegate())) .collect(Collectors.toList()); } private Collection<MutateWorker> createWorkersForDeletions(final StaticBuffer hashKey, final List<StaticBuffer> deletions, final DynamoDbStoreTransaction txh) { final List<MutateWorker> workers = new LinkedList<>(); for (StaticBuffer rangeKey : deletions) { final Map<String, AttributeValue> keys = new ItemBuilder().hashKey(hashKey) .rangeKey(rangeKey) .build(); final Expression updateExpression = new MultiUpdateExpressionBuilder(this, txh).hashKey(hashKey) .rangeKey(rangeKey) .build(); final DeleteItemRequest request = super.createDeleteItemRequest().withKey(keys) .withConditionExpression(updateExpression.getConditionExpression()) .withExpressionAttributeValues(updateExpression.getAttributeValues()); workers.add(new DeleteItemWorker(request, client.getDelegate())); } return workers; } @Override public CreateTableRequest getTableSchema() { return super.getTableSchema() .withAttributeDefinitions( new AttributeDefinition() .withAttributeName(Constants.JANUSGRAPH_HASH_KEY) .withAttributeType(ScalarAttributeType.S), new AttributeDefinition() .withAttributeName(Constants.JANUSGRAPH_RANGE_KEY) .withAttributeType(ScalarAttributeType.S)) .withKeySchema( new KeySchemaElement() .withAttributeName(Constants.JANUSGRAPH_HASH_KEY) .withKeyType(KeyType.HASH), new KeySchemaElement() .withAttributeName(Constants.JANUSGRAPH_RANGE_KEY) .withKeyType(KeyType.RANGE)); } }