All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.symphony.oss.fugue.aws.kv.table.AbstractDynamoDbKvTable Maven / Gradle / Ivy

There is a newer version: 0.3.0
Show newest version
/*
 *
 *
 * Copyright 2019 Symphony Communication Services, LLC.
 *
 * Licensed to The Symphony Software Foundation (SSF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.symphony.oss.fugue.aws.kv.table;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.function.Consumer;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.document.BatchWriteItemOutcome;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.dynamodbv2.document.Item;
import com.amazonaws.services.dynamodbv2.document.ItemCollection;
import com.amazonaws.services.dynamodbv2.document.ItemUtils;
import com.amazonaws.services.dynamodbv2.document.KeyAttribute;
import com.amazonaws.services.dynamodbv2.document.Page;
import com.amazonaws.services.dynamodbv2.document.PrimaryKey;
import com.amazonaws.services.dynamodbv2.document.QueryOutcome;
import com.amazonaws.services.dynamodbv2.document.Table;
import com.amazonaws.services.dynamodbv2.document.TableWriteItems;
import com.amazonaws.services.dynamodbv2.document.spec.GetItemSpec;
import com.amazonaws.services.dynamodbv2.document.spec.QuerySpec;
import com.amazonaws.services.dynamodbv2.document.utils.ValueMap;
import com.amazonaws.services.dynamodbv2.model.AmazonDynamoDBException;
import com.amazonaws.services.dynamodbv2.model.AttributeDefinition;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.BillingMode;
import com.amazonaws.services.dynamodbv2.model.CancellationReason;
import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
import com.amazonaws.services.dynamodbv2.model.CreateTableResult;
import com.amazonaws.services.dynamodbv2.model.Delete;
import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest;
import com.amazonaws.services.dynamodbv2.model.DeleteTableRequest;
import com.amazonaws.services.dynamodbv2.model.DescribeTimeToLiveRequest;
import com.amazonaws.services.dynamodbv2.model.DescribeTimeToLiveResult;
import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
import com.amazonaws.services.dynamodbv2.model.KeyType;
import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughputExceededException;
import com.amazonaws.services.dynamodbv2.model.Put;
import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException;
import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType;
import com.amazonaws.services.dynamodbv2.model.StreamSpecification;
import com.amazonaws.services.dynamodbv2.model.TableDescription;
import com.amazonaws.services.dynamodbv2.model.Tag;
import com.amazonaws.services.dynamodbv2.model.TagResourceRequest;
import com.amazonaws.services.dynamodbv2.model.TimeToLiveDescription;
import com.amazonaws.services.dynamodbv2.model.TimeToLiveSpecification;
import com.amazonaws.services.dynamodbv2.model.TransactWriteItem;
import com.amazonaws.services.dynamodbv2.model.TransactWriteItemsRequest;
import com.amazonaws.services.dynamodbv2.model.TransactionCanceledException;
import com.amazonaws.services.dynamodbv2.model.Update;
import com.amazonaws.services.dynamodbv2.model.UpdateTableRequest;
import com.amazonaws.services.dynamodbv2.model.UpdateTimeToLiveRequest;
import com.amazonaws.services.dynamodbv2.model.UpdateTimeToLiveResult;
import com.amazonaws.services.dynamodbv2.model.WriteRequest;
import com.symphony.oss.commons.fault.CodingFault;
import com.symphony.oss.commons.fault.FaultAccumulator;
import com.symphony.oss.commons.hash.Hash;
import com.symphony.oss.fugue.Fugue;
import com.symphony.oss.fugue.aws.AwsTags;
import com.symphony.oss.fugue.kv.IKvItem;
import com.symphony.oss.fugue.kv.IKvPagination;
import com.symphony.oss.fugue.kv.IKvPartitionKeyProvider;
import com.symphony.oss.fugue.kv.IKvPartitionSortKeyProvider;
import com.symphony.oss.fugue.kv.KvCondition;
import com.symphony.oss.fugue.kv.KvPagination;
import com.symphony.oss.fugue.kv.table.AbstractKvTable;
import com.symphony.oss.fugue.kv.table.IKvTableTransaction;
import com.symphony.oss.fugue.store.NoSuchObjectException;
import com.symphony.oss.fugue.store.TransactionFailedException;
import com.symphony.oss.fugue.trace.ITraceContext;
import com.symphony.oss.fugue.trace.NoOpTraceContext;

/**
 * DynamoDB implementation of IKvTable
 * 
 * @author Bruce Skingle
 *
 * @param  Concrete type for fluent methods.
 */
public abstract class AbstractDynamoDbKvTable> extends AbstractKvTable
{
  private static final Logger         log_                   = LoggerFactory.getLogger(AbstractDynamoDbKvTable.class);

  public static final String       ColumnNamePartitionKey = "pk";
  public static final String       ColumnNameSortKey      = "sk";
  public static final String       ColumnNameDocument     = "d";
  public static final String       ColumnNamePodId        = "p";
  public static final String       ColumnNamePayloadType  = "pt";
  public static final String       ColumnNameTTL          = "t";
  public static final String       ColumnNameCreatedDate  = "c";
  public static final String       ColumnNameAbsoluteHash = "h";

  protected static final int          MAX_RECORD_SIZE        = 400 * 1024;

  private static final String KEY_EXISTS = "An object with given partition and sort key already exists.";
  private static final String KEY_EXISTS_OR_OBJECT_CHANGED = "An object with given partition and sort key already exists, or the object to be updated has changed.";

  protected final String              region_;

  protected AmazonDynamoDB            amazonDynamoDB_;
  protected DynamoDB                  dynamoDB_;
  protected Table                     objectTable_;

  protected final String              objectTableName_;
  protected final int                 payloadLimit_;
  protected final boolean             validate_;
  protected final boolean             enableSecondaryStorage_;
  protected final StreamSpecification streamSpecification_;
  
  protected AbstractDynamoDbKvTable(AbstractBuilder builder)
  {
    super(builder);
    
    region_ = builder.region_;
    payloadLimit_ = builder.payloadLimit_;
    validate_ = builder.validate_;
    enableSecondaryStorage_ = builder.enableSecondaryStorage_;
    streamSpecification_ = builder.streamSpecification_;
  
    log_.info("Starting storage...");
    
    
    
    amazonDynamoDB_ = builder.amazonDynamoDBClientBuilder_.build();
    
    dynamoDB_               = new DynamoDB(amazonDynamoDB_);
    objectTableName_        = nameFactory_.getTableName("objects").toString();
    objectTable_            = dynamoDB_.getTable(objectTableName_);
    
        
    validate();
    
    log_.info("storage started.");
  }
  
  protected void validate()
  {
    if(validate_)
    {
      try(FaultAccumulator report = new FaultAccumulator())
      {
        try
        {
          objectTable_.describe();
        }
        catch(ResourceNotFoundException e)
        {
          report.error("Object table does not exist");
        }
      }
    }
  }

  /**
   * Return the name of this table.
   * 
   * @return the name of this table.
   */
  public String getTableName()
  {
    return objectTableName_;
  }

  @Override
  public String fetch(IKvPartitionSortKeyProvider partitionSortKey, ITraceContext trace) throws NoSuchObjectException
  {
    return doDynamoReadTask(() ->
    {
      GetItemSpec spec = new GetItemSpec().withPrimaryKey(ColumnNamePartitionKey, getPartitionKey(partitionSortKey), ColumnNameSortKey, partitionSortKey.getSortKey().toString());

      Item item = objectTable_.getItem(spec);
      
      if(item == null)
        throw new NoSuchObjectException("Item (" + getPartitionKey(partitionSortKey) + ", " + partitionSortKey.getSortKey() + ") not found.");
      
      String payloadString = item.getString(ColumnNameDocument);
      
      if(payloadString == null)
      {
        Hash absoluteHash = Hash.ofBase64String(item.getString(ColumnNameAbsoluteHash));
        
        payloadString = fetchFromSecondaryStorage(absoluteHash, trace);
      }
      
      return payloadString;
    });
  }

  @Override
  public String fetchFirst(IKvPartitionKeyProvider partitionKey, ITraceContext trace) throws NoSuchObjectException
  {
    return fetchOne(partitionKey, true, trace);
  }

  @Override
  public String fetchLast(IKvPartitionKeyProvider partitionKey, ITraceContext trace) throws NoSuchObjectException
  {
    return fetchOne(partitionKey, false, trace);
  }

  private String fetchOne(IKvPartitionKeyProvider partitionKey, boolean scanForwards, ITraceContext trace) throws NoSuchObjectException
  {
    return doDynamoReadTask(() ->
    {
      trace.trace("START_FETCH_ONE");
      QuerySpec spec = new QuerySpec()
        .withKeyConditionExpression(ColumnNamePartitionKey + " = :v_partition")
        .withMaxResultSize(1)
        .withValueMap(new ValueMap()
            .withString(":v_partition", getPartitionKey(partitionKey)))
        .withScanIndexForward(scanForwards)
        ;
    
      ItemCollection items = objectTable_.query(spec);
      
      Iterator it = items.firstPage().iterator();
      
      if(it.hasNext())
      {
        Item item = it.next();
        
        String payloadString = item.getString(ColumnNameDocument);
            
        if(payloadString == null)
        {
          Hash absoluteHash = Hash.ofBase64String(item.getString(ColumnNameAbsoluteHash));
          
          payloadString = fetchFromSecondaryStorage(absoluteHash, trace);
        }
        
        trace.trace("DONE_FETCH_ONE");
        return payloadString;
      }
      
      throw new NoSuchObjectException(partitionKey + " not found");
    });
  }
  
  protected  CT doDynamoQueryTask(Callable task)
  {
    try
    {
      return doDynamoReadTask(task);
    }
    catch (NoSuchObjectException e)
    {
      // This "can't happen"
      throw new CodingFault(e);
    }
  }

  protected  CT doDynamoReadTask(Callable task) throws NoSuchObjectException
  {
    return doDynamoReadTask(task, NoOpTraceContext.INSTANCE);
  }

  protected  CT doDynamoReadTask(Callable task, ITraceContext trace) throws NoSuchObjectException
  {
    return doDynamoTask(task, "read", trace);
  }

  @Override
  public Transaction createTransaction()
  {
    return new Transaction();
  }

  @Override
  public void store(Collection kvItems, ITraceContext trace)
  {
    try
    {
//      store(null, kvItems, trace);
      
      Transaction transaction = new Transaction();
      
      transaction.store(null, kvItems);
      
      transaction.commit(trace);
    }
    catch (TransactionFailedException e)
    {
      throw new IllegalStateException(e);
    }
  }

//  @Override
//  public void store(@Nullable IKvPartitionSortKeyProvider partitionSortKeyProvider, Collection kvItems, ITraceContext trace) throws ObjectExistsException
//  {
//    if(kvItems.isEmpty())
//      return;
//
//    Hash        absoluteHash = null;
//    List actions = new ArrayList<>(kvItems.size());
//    String    existingPartitionKey = partitionSortKeyProvider==null ? null : getPartitionKey(partitionSortKeyProvider);
//    String    existingSortKey = partitionSortKeyProvider==null ? null : partitionSortKeyProvider.getSortKey().asString();
//      
//    
//    Hash secondaryStoredHash = null;
//    
//    for(IKvItem kvItem : kvItems)
//    {
//      String partitionKey = getPartitionKey(kvItem);
//      String sortKey = kvItem.getSortKey().asString();
//      
//      UpdateOrPut updateOrPut = new UpdateOrPut(kvItem, partitionKey, sortKey, payloadLimit_);
//      
//      absoluteHash = kvItem.getAbsoluteHash();
//      
//      if(kvItem.isSaveToSecondaryStorage())
//      {
//        if(storeToSecondaryStorage(kvItem, updateOrPut.payloadNotStored_, trace))
//          secondaryStoredHash = kvItem.getAbsoluteHash();
//      }
//      
//      Put put = updateOrPut.createPut();
//      
//      // It's not strictly necessary to check both things for null but it stops the compiler complaining...
//      if(existingPartitionKey!=null && existingSortKey!=null && existingPartitionKey.equals(partitionKey) && existingSortKey.equals(sortKey))
//      {
//        put.withConditionExpression("attribute_not_exists(" + ColumnNamePartitionKey + ") and attribute_not_exists(" + ColumnNameSortKey + ")");
//      }
//      
//      actions.add(new TransactWriteItem().withPut(put));
//    }
//    
//    try
//    {
//      write(actions, absoluteHash.toStringBase64(), KEY_EXISTS, trace);
//    }
//    catch (NoSuchObjectException e)
//    {
//      log_.error("Failed to wite objects", e);
//      if(secondaryStoredHash != null)
//      {
//        try
//        {
//          deleteFromSecondaryStorage(secondaryStoredHash, trace);
//        }
//        catch(RuntimeException e2)
//        {
//          log_.error("Failed to delete secondary copy of " + secondaryStoredHash, e2);
//        }
//      }
//      throw new ObjectExistsException(KEY_EXISTS, e);
//    }
//  }
  
  @Override
  public void store(IKvItem kvItem, KvCondition kvCondition, ITraceContext trace)
  {
    Hash        absoluteHash = kvItem.getAbsoluteHash();
    List actions = new ArrayList<>(1);
    Hash secondaryStoredHash = null;
    
    String partitionKey = getPartitionKey(kvItem);
    String sortKey = kvItem.getSortKey().asString();
    
    UpdateOrPut updateOrPut = new UpdateOrPut(kvItem, partitionKey, sortKey, payloadLimit_);
    
    ;
    
    if(kvItem.isSaveToSecondaryStorage())
    {
      if(storeToSecondaryStorage(kvItem, updateOrPut.payloadNotStored_, trace))
        secondaryStoredHash = kvItem.getAbsoluteHash();
    }
    
    Condition condition = new Condition(
        "attribute_not_exists(" + kvCondition.getName() + ") or " +
        kvCondition.getName() + " " + kvCondition.getComparison().getSymbol() + " :v")
        .withString(":v", kvCondition.getValue());
    
    Put put = updateOrPut
        .createPut()
        .withConditionExpression(condition.expression_)
        .withExpressionAttributeValues(condition.attributeValues_);
    
    actions.add(new TransactWriteItem().withPut(put));
    
    try
    {
      trace.trace("ABOUT_TO_STORE_CONDITIONAL", kvItem);
      write(actions, absoluteHash.toStringBase64(), "Conditions not met.", trace);
      trace.trace("STORED_CONDITIONAL", kvItem);
    }
    catch (NoSuchObjectException e)
    {
      trace.trace("FAILED_TO_STORE_CONDITIONAL", kvItem);
      if(secondaryStoredHash != null)
      {
        try
        {
          deleteFromSecondaryStorage(secondaryStoredHash, trace);
        }
        catch(RuntimeException e2)
        {
          log_.error("Failed to delete secondary copy of " + secondaryStoredHash, e2);
        }
      }
    }
  }

  class Condition
  {
    String                      expression_;
    Map attributeValues_ = new HashMap<>();
    
    Condition(String expression)
    {
      expression_ = expression;
    }
    
    Condition withString(String name, String value)
    {
      attributeValues_.put(name, new AttributeValue().withS(value));
      
      return this;
    }
  }
  
  class DeleteConsumer extends AbstractItemConsumer
  {
    List            primaryKeysToDelete_ = new ArrayList<>(24);
    IKvPartitionSortKeyProvider absoluteHashPrefix_;
    
    public DeleteConsumer(IKvPartitionSortKeyProvider absoluteHashPrefix)
    {
      absoluteHashPrefix_ = absoluteHashPrefix;
    }

    @Override
    void consume(Item item, ITraceContext trace)
    {
      primaryKeysToDelete_.add(new PrimaryKey(
          new KeyAttribute(ColumnNamePartitionKey,  item.getString(ColumnNamePartitionKey)),
          new KeyAttribute(ColumnNameSortKey,       item.getString(ColumnNameSortKey))
          )
        );
      
      Hash absoluteHash = Hash.newInstance(item.getString(ColumnNameAbsoluteHash));
      
      primaryKeysToDelete_.add(new PrimaryKey(
          new KeyAttribute(ColumnNamePartitionKey,  getPartitionKey(absoluteHashPrefix_) + absoluteHash),
          new KeyAttribute(ColumnNameSortKey,       absoluteHashPrefix_.getSortKey().asString())
          )
        );
      
      deleteFromSecondaryStorage(absoluteHash, trace);
    }
    
    void dynamoBatchWrite()
    {
      if(primaryKeysToDelete_.isEmpty())
        return;
      
      TableWriteItems tableWriteItems = new TableWriteItems(objectTable_.getTableName())
          .withPrimaryKeysToDelete(primaryKeysToDelete_.toArray(new PrimaryKey[primaryKeysToDelete_.size()]));
      
      BatchWriteItemOutcome outcome = dynamoDB_.batchWriteItem(tableWriteItems);
      int totalRequestItems = primaryKeysToDelete_.size();
      long  delay = 4;
      do
      {
          Map> unprocessedItems = outcome.getUnprocessedItems();

          if (outcome.getUnprocessedItems().size() > 0)
          {
            int requestItems = 0;
            
            for(List ui : unprocessedItems.values())
            {
              requestItems += ui.size();
            }
      
            log_.info("Retry " + requestItems + " of " + totalRequestItems + " items after " + delay + "ms.");
            
            totalRequestItems = requestItems;
            
            try
            {
              Thread.sleep(delay);
              
              if(delay < 1000)
                delay *= 1.2;
            }
            catch (InterruptedException e)
            {
              log_.warn("Sleep interrupted", e);
            }
            
            outcome = dynamoDB_.batchWriteItemUnprocessed(unprocessedItems);
          }
      } while (outcome.getUnprocessedItems().size() > 0);
    }
  }
  
  @Override
  public void delete(IKvPartitionSortKeyProvider partitionSortKeyProvider, 
      IKvPartitionKeyProvider versionPartitionKey, IKvPartitionSortKeyProvider absoluteHashPrefix, ITraceContext trace)
  {
    String    existingPartitionKey = getPartitionKey(partitionSortKeyProvider);
    String    existingSortKey = partitionSortKeyProvider.getSortKey().asString();
    
    String after = null;
    do
    {
      DeleteConsumer deleteConsumer = new DeleteConsumer(absoluteHashPrefix);
      
      after = doFetchPartitionObjects(versionPartitionKey, true, 12, after, null, null, deleteConsumer, trace).getAfter();
      
      deleteConsumer.dynamoBatchWrite();
    } while (after != null);
    
    final Map itemKey = new HashMap<>();
    
    itemKey.put(ColumnNamePartitionKey, new AttributeValue(existingPartitionKey));
    itemKey.put(ColumnNameSortKey, new AttributeValue(existingSortKey));
    
    amazonDynamoDB_.deleteItem(new DeleteItemRequest()
        .withTableName(objectTableName_)
        .withKey(itemKey)
        );
  }
  
  @Override
  public void deleteRow(IKvPartitionSortKeyProvider partitionSortKeyProvider, ITraceContext trace)
  {
    String    existingPartitionKey = getPartitionKey(partitionSortKeyProvider);
    String    existingSortKey = partitionSortKeyProvider.getSortKey().asString();
    
    final Map itemKey = new HashMap<>();
    
    itemKey.put(ColumnNamePartitionKey, new AttributeValue(existingPartitionKey));
    itemKey.put(ColumnNameSortKey, new AttributeValue(existingSortKey));
    
    amazonDynamoDB_.deleteItem(new DeleteItemRequest()
        .withKey(itemKey)
        .withTableName(objectTable_.getTableName())
        );
  }
  
  public class Transaction implements IKvTableTransaction
  {
    String                  id_ = UUID.randomUUID().toString();
    List actions_ = new LinkedList<>();
    List           secondaryStorageItemNotStored_ = new LinkedList<>();
    List           secondaryStorageItemStored_ = new LinkedList<>();

    @Override
    public void commit(ITraceContext trace) throws TransactionFailedException
    {
      List secondaryStoredHashes = new LinkedList<>();
      
      for(IKvItem kvItem : secondaryStorageItemNotStored_)
      {
        if(storeToSecondaryStorage(kvItem, true, trace))
          secondaryStoredHashes.add(kvItem.getAbsoluteHash());
      }
      
      for(IKvItem kvItem : secondaryStorageItemStored_)
      {
        if(storeToSecondaryStorage(kvItem, false, trace))
          secondaryStoredHashes.add(kvItem.getAbsoluteHash());
      }
      
      try
      {
        write(actions_, id_, KEY_EXISTS_OR_OBJECT_CHANGED, trace);
      }
      catch(AmazonDynamoDBException e)
      {
        int i = e.getErrorMessage().lastIndexOf(':');
        
        if(i != -1)
        {
          String s =  e.getErrorMessage().substring(i);
          
          if(s.startsWith(": Member must have length less than or equal to "))
          {
            throw new IllegalArgumentException("Transaction too large" + s);
          }
        }
        
        throw e;
      }
      catch (NoSuchObjectException e)
      {
        log_.error("Failed to wite objects", e);
        for(Hash secondaryStoredHash : secondaryStoredHashes)
        {
          try
          {
            deleteFromSecondaryStorage(secondaryStoredHash, trace);
          }
          catch(RuntimeException e2)
          {
            log_.error("Failed to delete secondary copy of " + secondaryStoredHash, e2);
          }
        }
        throw new TransactionFailedException(KEY_EXISTS_OR_OBJECT_CHANGED, e);
      }
    }

    @Override
    public void store(@Nullable IKvPartitionSortKeyProvider partitionSortKeyProvider, Collection kvItems)
    {
      if(kvItems.isEmpty())
        return;

      //Hash        absoluteHash = null;
      String    existingPartitionKey = partitionSortKeyProvider==null ? null : getPartitionKey(partitionSortKeyProvider);
      String    existingSortKey = partitionSortKeyProvider==null ? null : partitionSortKeyProvider.getSortKey().asString();

      
      for(IKvItem kvItem : kvItems)
      {
        String partitionKey = getPartitionKey(kvItem);
        String sortKey = kvItem.getSortKey().asString();
        
        UpdateOrPut updateOrPut = new UpdateOrPut(kvItem, partitionKey, sortKey, payloadLimit_);
        
        if(kvItem.isSaveToSecondaryStorage())
        {
          if(updateOrPut.payloadNotStored_)
            secondaryStorageItemNotStored_.add(kvItem);
          else
            secondaryStorageItemStored_.add(kvItem);
        }
        
        Put put = updateOrPut.createPut();
        
        // It's not strictly necessary to check both things for null but it stops the compiler complaining...
        if(existingPartitionKey!=null && existingSortKey!=null && existingPartitionKey.equals(partitionKey) && existingSortKey.equals(sortKey))
        {
          put.withConditionExpression("attribute_not_exists(" + ColumnNamePartitionKey + ") and attribute_not_exists(" + ColumnNameSortKey + ")");
        }
        
        actions_.add(new TransactWriteItem().withPut(put));
      }
    }
    
    @Override
    public void update(IKvPartitionSortKeyProvider partitionSortKeyProvider, Hash absoluteHash, Set kvItems)
    {
      String    existingPartitionKey = getPartitionKey(partitionSortKeyProvider);
      String    existingSortKey = partitionSortKeyProvider.getSortKey().asString();
      Condition condition = new Condition(ColumnNameAbsoluteHash + " = :ah").withString(":ah", absoluteHash.toStringBase64());
      
      for(IKvItem kvItem : kvItems)
      {
        String partitionKey = getPartitionKey(kvItem);
        String sortKey = kvItem.getSortKey().asString();
        
        UpdateOrPut updateOrPut = new UpdateOrPut(kvItem, partitionKey, sortKey, payloadLimit_);
        
        if(existingPartitionKey.equals(partitionKey) && existingSortKey.equals(sortKey))
        {
          actions_.add(new TransactWriteItem()
              .withUpdate(
                  updateOrPut.createUpdate(condition)
                  )
              );
          
          condition = null;
        }
        else
        {
          Put put = updateOrPut.createPut();
          
          if(existingPartitionKey.equals(partitionKey))
          {
            put.withConditionExpression("attribute_not_exists(" + ColumnNamePartitionKey + ") and attribute_not_exists(" + ColumnNameSortKey + ")");
          }
              
          actions_.add(new TransactWriteItem()
              .withPut(put)
              );
                  
        }
        
        if(kvItem.isSaveToSecondaryStorage())
        {
          if(updateOrPut.payloadNotStored_)
            secondaryStorageItemNotStored_.add(kvItem);
          else
            secondaryStorageItemStored_.add(kvItem);
        }
      }
      
      if(condition != null)
      {
        // The prev version has a different sort key

        final Map itemKey = new HashMap<>();
        
        itemKey.put(ColumnNamePartitionKey, new AttributeValue(existingPartitionKey));
        itemKey.put(ColumnNameSortKey, new AttributeValue(existingSortKey));
       
        Delete delete = new Delete()
            .withTableName(objectTable_.getTableName())
            .withConditionExpression(condition.expression_)
            .withExpressionAttributeValues(condition.attributeValues_)
            .withKey(itemKey)
            ;
        
        actions_.add(new TransactWriteItem().withDelete(delete));
      }
    }
  }
  
//  @Override
//  public void update(IKvPartitionSortKeyProvider partitionSortKeyProvider, Hash absoluteHash, Set kvItems,
//      ITraceContext trace) throws NoSuchObjectException
//  {
//    List actions = new ArrayList<>(kvItems.size() + 2);
//    
//    String    existingPartitionKey = getPartitionKey(partitionSortKeyProvider);
//    String    existingSortKey = partitionSortKeyProvider.getSortKey().asString();
//    Condition condition = new Condition(ColumnNameAbsoluteHash + " = :ah").withString(":ah", absoluteHash.toStringBase64());
//    
//    Hash secondaryStoredHash = null;
//    
//    for(IKvItem kvItem : kvItems)
//    {
//      String partitionKey = getPartitionKey(kvItem);
//      String sortKey = kvItem.getSortKey().asString();
//      
//      UpdateOrPut updateOrPut = new UpdateOrPut(kvItem, partitionKey, sortKey, payloadLimit_);
//      
//      if(existingPartitionKey.equals(partitionKey) && existingSortKey.equals(sortKey))
//      {
//        actions.add(new TransactWriteItem()
//            .withUpdate(
//                updateOrPut.createUpdate(condition)
//                )
//            );
//        
//        condition = null;
//      }
//      else
//      {
//        Put put = updateOrPut.createPut();
//        
//        if(existingPartitionKey.equals(partitionKey))
//        {
//          put.withConditionExpression("attribute_not_exists(" + ColumnNamePartitionKey + ") and attribute_not_exists(" + ColumnNameSortKey + ")");
//        }
//            
//        actions.add(new TransactWriteItem()
//            .withPut(put)
//            );
//                
//      }
//      
//      if(kvItem.isSaveToSecondaryStorage())
//      {
//        if(storeToSecondaryStorage(kvItem, updateOrPut.payloadNotStored_, trace))
//          secondaryStoredHash = kvItem.getAbsoluteHash();
//      }
//    }
//    
//    String error = "Object to be updated (" + absoluteHash + ") has changed.";
//    
//    if(condition != null)
//    {
//      // The prev version has a different sort key
//
//      final Map itemKey = new HashMap<>();
//      
//      itemKey.put(ColumnNamePartitionKey, new AttributeValue(existingPartitionKey));
//      itemKey.put(ColumnNameSortKey, new AttributeValue(existingSortKey));
//     
//      Delete delete = new Delete()
//          .withTableName(objectTable_.getTableName())
//          .withConditionExpression(condition.expression_)
//          .withExpressionAttributeValues(condition.attributeValues_)
//          .withKey(itemKey)
//          ;
//      
//      actions.add(new TransactWriteItem().withDelete(delete));
//      
//      error = "Object to be updated (" + absoluteHash + ") has changed or an object already exists with the new sort key.";
//    }
//    
//    try
//    {
//      write(actions, absoluteHash.toStringBase64(), error, trace);
//    }
//    catch (NoSuchObjectException e)
//    {
//      if(secondaryStoredHash != null)
//      {
//        try
//        {
//          deleteFromSecondaryStorage(secondaryStoredHash, trace);
//        }
//        catch(RuntimeException e2)
//        {
//          log_.error("Failed to delete secondary copy of " + secondaryStoredHash, e2);
//        }
//      }
//      throw e;
//    }
//  }
  
  protected void write(Collection actions, String txnId, String errorMessage, ITraceContext trace) throws NoSuchObjectException
  {
    TransactWriteItemsRequest request = new TransactWriteItemsRequest()
        .withTransactItems(actions);
     
    doDynamoConditionalWriteTask(() -> 
    {
      int                           retryCnt = 0;
      long                          delay = 4;
      TransactionCanceledException  lastException = null;
      
      while(retryCnt++ < 11)
      {
        try
        {
          trace.trace("ABOUT_TO_STORE_TRANSACTIONAL", "OBJECT", txnId);
          amazonDynamoDB_.transactWriteItems(request);
          trace.trace("STORED_TRANSACTIONAL", "OBJECT", txnId);
          return null;
        }
        catch (TransactionCanceledException tce)
        {
          lastException = tce;
          
          for(CancellationReason reason : tce.getCancellationReasons())
          {
            switch(reason.getCode())
            {
              case "ConditionalCheckFailed":
                trace.trace("FAILED_FATAL_STORE_TRANSACTIONAL", "OBJECT", txnId);
                throw new NoSuchObjectException(errorMessage);
                
              case "None":
                // there is an entry for each item in the transaction, this means nothing and should be ignored.
                break;
                
              case "TransactionConflict":
                log_.info("Retry transaction after " + delay + "ms.");

                trace.trace("WAIT_RETRY_TO_STORE_TRANSACTIONAL", "OBJECT", txnId);
                try
                {
                  Thread.sleep(delay);
                  
                  if(delay < 1000)
                    delay *= 1.2;
                }
                catch (InterruptedException e)
                {
                  log_.warn("Sleep interrupted", e);
                }
                break;
                
              default:
                trace.trace("FAILED_TRANSIENT_STORE_TRANSACTIONAL", "OBJECT", txnId);
                throw new IllegalStateException("Transient failure to store object " + txnId, tce);
            }
          }
        }
        catch(RuntimeException e)
        {
          trace.trace("FAILED_TRANSIENT_STORE_TRANSACTIONAL", "OBJECT", txnId);
          throw e;
        }
      }
      trace.trace("FAILED_TRANSIENT_STORE_TRANSACTIONAL", "OBJECT", txnId);
      throw new IllegalStateException("Transient failure to update (after " + retryCnt + " retries) object " + txnId, lastException);
    }
    , trace);  
    
  }

  /**
   * Fetch the given item from secondary storage.
   * 
   * @param absoluteHash      Absolute hash of the required object.
   * @param trace             Trace context.
   * 
   * @return The required object.
   * 
   * @throws NoSuchObjectException If the required object does not exist.
   */
  protected abstract @Nonnull String fetchFromSecondaryStorage(Hash absoluteHash, ITraceContext trace) throws NoSuchObjectException;
  
  /**
   * Store the given item to secondary storage.
   * 
   * @param kvItem            An item to be stored.
   * @param payloadNotStored  The payload is too large to store in primary storage.
   * @param trace             A trace context.
   * @return true if the object was stored
   */
  protected abstract boolean storeToSecondaryStorage(IKvItem kvItem, boolean payloadNotStored, ITraceContext trace);

  /**
   * Delete the given object from secondary storage.
   * 
   * @param absoluteHash  Hash of the item to be deleted.
   * @param trace         A trace context.
   */
  protected abstract void deleteFromSecondaryStorage(Hash absoluteHash, ITraceContext trace);
  
//  protected void write(List items, ITraceContext trace)
//  {
//    doDynamoWriteTask(() -> 
//    {
//      dynamoBatchWrite(items);
//      trace.trace("WRITTEN-DYNAMODB");
//      
//      return null;
//     }
//    , trace);
//  }

  protected  TT doDynamoTask(Callable task, String accessMode, ITraceContext trace) throws NoSuchObjectException
  {
    String message = "Failed to " + accessMode + " object";
    
    try
    {
      return task.call();
    }
    catch(ProvisionedThroughputExceededException e)
    {
      log_.warn(message + " - Provisioned Throughput Exceeded", e);
      trace.trace("FAILED-THROUGHPUT-DYNAMODB");
//      try
//      {
//        objectTableHelper_.scaleOutWrite();
//      }
//      catch(RuntimeException e2)
//      {
//        log_.error("Failed to scale out", e2);
//      }
      
      throw new IllegalStateException(message, e);
    } 
    catch (AmazonServiceException e)
    {
      trace.trace("FAILED-AWSEXCEPTION-DYNAMODB");
      log_.error(message, e);
      throw e;
    } 
    catch (NoSuchObjectException e)
    {
      throw e;
    } 
    catch (Exception e) // Callable made me do this...
    {
      trace.trace("FAILED-UNEXPECTED-DYNAMODB");
      log_.error("UNEXPECTED EXCEPTION", e);
      throw new IllegalStateException(message, e);
    }
  }
  
  protected void doDynamoWriteTask(Callable task, ITraceContext trace)
  {
    try
    {
      doDynamoTask(task, "write", trace);
    }
    catch (NoSuchObjectException e)
    {
      trace.trace("FAILED-UNEXPECTED-DYNAMODB");
      log_.error("UNEXPECTED EXCEPTION", e);
      throw new IllegalStateException("Failed to write object", e);
    }
  }
  
  protected void doDynamoConditionalWriteTask(Callable task, ITraceContext trace) throws NoSuchObjectException
  {
    try
    {
      doDynamoTask(task, "write", trace);
    }
    catch (NoSuchObjectException e)
    {
      throw e;
    }
  }

//  protected void dynamoBatchWrite(Collection itemsToPut)
//  {
//    TableWriteItems tableWriteItems = new TableWriteItems(objectTable_.getTableName())
//        .withItemsToPut(itemsToPut)
//        ;
//    
//    if(primaryKeysToDelete != null)
//      tableWriteItems = tableWriteItems.withPrimaryKeysToDelete(primaryKeysToDelete.toArray(new PrimaryKey[primaryKeysToDelete.size()]));
//    
//    Map> requestItems = new HashMap<>();
//    
//    new WriteRequest().withPutRequest(new PutRequest()
//    
//    List value;
//    requestItems.put(objectTable_.getTableName(), value);
//    BatchWriteItemRequest batchWriteItemRequest = new BatchWriteItemRequest().withRequestItems(requestItems);
//    BatchWriteItemResult outcome = amazonDynamoDB_.batchWriteItem(batchWriteItemRequest);
//    
//    
//    int requestItems = itemsToPut.size();
//    long  delay = 4;
//    do
//    {
//        Map> unprocessedItems = outcome.getUnprocessedItems();
//
//        if (outcome.getUnprocessedItems().size() > 0)
//        {
//          requestItems = 0;
//          
//          for(List ui : unprocessedItems.values())
//          {
//            requestItems += ui.size();
//          }
//    
//          log_.info("Retry " + requestItems + " of " + requestItems + " items after " + delay + "ms.");
//          try
//          {
//            Thread.sleep(delay);
//            
//            if(delay < 1000)
//              delay *= 1.2;
//          }
//          catch (InterruptedException e)
//          {
//            log_.warn("Sleep interrupted", e);
//          }
//          
//          outcome = dynamoDB_.batchWriteItemUnprocessed(unprocessedItems);
//        }
//    } while (outcome.getUnprocessedItems().size() > 0);
//  }
  
  class UpdateOrPut
  {
    private static final String INVALID_ATTR_KEY_LEN = "Additional attribute keys must be between 3 and 10 characters";
    
    Map key_              = new HashMap<>();
    ValueMap                    putItem_          = new ValueMap();
    Map updateItem_       = new HashMap<>();
    StringBuilder               updateExpression_ = new StringBuilder("SET ");
    int                         baseLength_;
    boolean                     payloadNotStored_;
    boolean                     first_            = true;
    
    UpdateOrPut(IKvItem kvItem, String partitionKey, String sortKey, int payloadLimit)
    {
      putItem_.withString(ColumnNamePartitionKey,  partitionKey);
      putItem_.withString(ColumnNameSortKey,       sortKey);
      
      key_.put(ColumnNamePartitionKey,  new AttributeValue(partitionKey));
      key_.put(ColumnNameSortKey,       new AttributeValue(sortKey));
      
      baseLength_ = ColumnNamePartitionKey.length() + partitionKey.length() + 
          ColumnNameSortKey.length() + sortKey.length();
      
      withHash(   ColumnNameAbsoluteHash, kvItem.getAbsoluteHash());
      
      if(kvItem.getPodId() != null)
        withNumber( ColumnNamePodId,        kvItem.getPodId().getValue());
      
      withString( ColumnNamePayloadType,  kvItem.getType());
      
      if(kvItem.getAdditionalAttributes() != null)
      {
        for(Entry entry : kvItem.getAdditionalAttributes().entrySet())
        {
          if(entry.getKey().length() < 3 || entry.getKey().length() >10)
            throw new IllegalArgumentException(INVALID_ATTR_KEY_LEN);
          
          if(entry.getValue() instanceof Number)
          {
            withNumber(entry.getKey(), (Number) entry.getValue());
          }
          else
          {
            withString(entry.getKey(), entry.getValue().toString());
          }
        }
      }
      
      if(kvItem.getPurgeDate() != null)
      {
        Long ttl = kvItem.getPurgeDate().toEpochMilli() / 1000;
        
        withNumber( ColumnNameTTL,          ttl);
      }
      
      int length = baseLength_ + ColumnNameDocument.length() + kvItem.getJson().length();
      
      if(length < payloadLimit)
      {
        payloadNotStored_ = false;
        withString(ColumnNameDocument, kvItem.getJson());
      }
      else
      {
        payloadNotStored_ = true;
      }
    }
    
    Put createPut()
    {
      return new Put()
        .withTableName(objectTable_.getTableName())
        .withItem(ItemUtils.fromSimpleMap(putItem_));
    }
    
    Update createUpdate(Condition condition)
    {
      updateItem_.putAll(condition.attributeValues_);
      
      return new Update()
        .withTableName(objectTable_.getTableName())
        .withConditionExpression(condition.expression_)
        .withExpressionAttributeValues(updateItem_)
        .withKey(key_)
        .withUpdateExpression(updateExpression_.toString())
      ;
    }

    private void withNumber(String name, Number value)
    {
      separator();
      updateExpression_.append(name + " = :" + name);
      
      if(value == null)
      {
        putItem_.withNull(name);
        updateItem_.put(":" + name, new AttributeValue().withNULL(true));
        baseLength_ += name.length();
      }
      else
      {
        putItem_.withNumber(name, value);
        updateItem_.put(":" + name, new AttributeValue().withN(value.toString()));
        baseLength_ += name.length() + value.toString().length();
      }
    }

    private void separator()
    {
      if(first_)
        first_ = false;
      else
        updateExpression_.append(", ");
      
    }

    private void withHash(String name, Hash value)
    {
      withString(name, value == null ? null : value.toStringBase64());
    }

    private void withString(String name, String value)
    {
      separator();
      updateExpression_.append(name + " = :" + name);
    
      if(value == null)
      {
        baseLength_ += name.length();
        putItem_.withNull(name);
        updateItem_.put(":" + name, new AttributeValue().withNULL(true));
      }
      else
      {
        baseLength_ += name.length() + value.length();
        putItem_.withString(name, value);
        updateItem_.put(":" + name, new AttributeValue().withS(value));
      }
    }

//    private void withJSON(String name, String value)
//    {
//      updateExpression_.append(name + " = :" + name);
//      
//      if(value == null)
//      {
//        baseLength_ += name.length();
//        putItem_.withNull(name);
//        updateItem_.put(":" + name, new AttributeValue().withNULL(true));
//      }
//      else
//      {
//        baseLength_ += name.length() + value.length();
//        putItem_.withJSON(name, value);
//        updateItem_.put(":" + name, new AttributeValue().with(value));
//        updateItem_.withJSON(":" + name, value);
//      }
//    }
  }
  
//  protected boolean createPut(IKvItem kvItem, String partitionKey, String sortKey, int payloadLimit, List items, Condition condition)
//  {
//    ValueMap putItem = new ValueMap();
//    ValueMap updateItem = new ValueMap();
//    StringBuilder updateExpression = new StringBuilder();
//    
//    putItem.withString(ColumnNamePartitionKey,  partitionKey);
//    putItem.withString(ColumnNameSortKey,       sortKey);
//    
//    int baseLength = ColumnNamePartitionKey.length() + partitionKey.length() + 
//        ColumnNameSortKey.length() + sortKey.length();
//    
//    if(kvItem.getAbsoluteHash() == null)
//    {
//      updateExpression.append(ColumnNameAbsoluteHash + " = null");
//    }
//    else
//    {
//      String ah = kvItem.getAbsoluteHash().toStringBase64();
//      
//      baseLength += ColumnNameAbsoluteHash.length() + ah.length();
//      putItem.withString(ColumnNameAbsoluteHash, ah);
//    }
//    
//    if(kvItem.getPodId() != null)
//    {
//      baseLength += ColumnNamePodId.length() + kvItem.getPodId().toString().length();
//      
//      putItem.withNumber(ColumnNamePodId, kvItem.getPodId().getValue());
//    }
//
//    if(kvItem.getType() != null)
//    {
//      baseLength += ColumnNamePayloadType.length() + kvItem.getType().length();
//      
//      putItem.withString(ColumnNamePayloadType, kvItem.getType());
//    }
//    
//    if(kvItem.getPurgeDate() != null)
//    {
//      long ttl = kvItem.getPurgeDate().toEpochMilli() / 1000;
//      
//      baseLength += ColumnNameTTL.length() + String.valueOf(ttl).length();
//      
//      putItem.withNumber(ColumnNameTTL, ttl);
//    }
//    
//    int length = baseLength + ColumnNameDocument.length() + kvItem.getJson().length();
//    
//    if(length < payloadLimit)
//    {
//      putItem.withJSON(ColumnNameDocument, kvItem.getJson());
//    }
//    
//    
//    
//    if(condition == null)
//    {
//      
//      Put put = new Put()
//          .withTableName(objectTable_.getTableName())
//          .withItem(ItemUtils.fromSimpleMap(putItem));
//    }
//    else
//    {
//      Update update = new Update()
//          .withTableName(objectTable_.getTableName())
//          .with
//          ;
//      put
//        .withConditionExpression(condition.expression_)
//        .withExpressionAttributeValues(condition.attributeValues_)
//        ;
//    }
//    
//    items.add(put);
//    
//    return length >= payloadLimit;
//  }
  

//  protected Put createPut2(IKvItem kvItem, String partitionKey, String sortKey, int payloadLimit)
//  {
//    HashMap item = new HashMap<>();
//    
//    item.put(ColumnNamePartitionKey,  new AttributeValue(partitionKey));
//    item.put(ColumnNameSortKey,       new AttributeValue(sortKey));
//    
//    int baseLength = ColumnNamePartitionKey.length() + partitionKey.length() + 
//        ColumnNameSortKey.length() + sortKey.length();
//    
//    if(kvItem.getAbsoluteHash() != null)
//    {
//      String ah = kvItem.getAbsoluteHash().toStringBase64();
//      
//      baseLength += ColumnNameAbsoluteHash.length() + ah.length();
//      item.put(ColumnNameAbsoluteHash, new AttributeValue(ah));
//    }
//    
//    if(kvItem.getPodId() != null)
//    {
//      baseLength += ColumnNamePodId.length() + kvItem.getPodId().toString().length();
//      
//      item.put(ColumnNamePodId, new AttributeValue().withN(kvItem.getPodId().getValue()));
//    }
//
//    if(kvItem.getType() != null)
//    {
//      baseLength += ColumnNamePayloadType.length() + kvItem.getType().length();
//      
//      item.put(ColumnNamePayloadType, new AttributeValue(kvItem.getType()));
//    }
//    
//    if(kvItem.getPurgeDate() != null)
//    {
//      String ttl = String.valueOf(kvItem.getPurgeDate().toEpochMilli() / 1000);
//      
//      baseLength += ColumnNameTTL.length() + ttl.length();
//      
//      item.put(ColumnNameTTL, new AttributeValue().withN(ttl));
//    }
//    
//    int length = baseLength + ColumnNameDocument.length() + kvItem.getJson().length();
//    
//    if(length < payloadLimit)
//    {
//      
//      item.put(ColumnNameDocument, new AttributeValue().withM(
//          valueConformer.transform(Jackson.fromJsonString(kvItem.getJson(), Object.class))));
//      return false;
//    }
//    else
//    {
//      return true;
//    }
//  }

//  protected boolean createPutItem(List items, IKvItem kvItem, String partitionKey, String sortKey, int payloadLimit)
//  {
//    Item item = new Item()
//        .withPrimaryKey(ColumnNamePartitionKey, 
//            partitionKey, 
//            ColumnNameSortKey, sortKey);
//    
//    int baseLength = ColumnNamePartitionKey.length() + partitionKey.length() + 
//        ColumnNameSortKey.length() + sortKey.length();
//    
//    if(kvItem.getAbsoluteHash() != null)
//    {
//      String ah = kvItem.getAbsoluteHash().toStringBase64();
//      
//      baseLength += ColumnNameAbsoluteHash.length() + ah.length();
//      
//      item.withString(ColumnNameAbsoluteHash, ah);
//    }
//    
//    if(kvItem.getPodId() != null)
//    {
//      baseLength += ColumnNamePodId.length() + kvItem.getPodId().toString().length();
//      
//      item.withInt(ColumnNamePodId, kvItem.getPodId().getValue());
//    }
//
//    if(kvItem.getType() != null)
//    {
//      baseLength += ColumnNamePayloadType.length() + kvItem.getType().length();
//      
//      item.withString(ColumnNamePayloadType, kvItem.getType());
//    }
//    
//    if(kvItem.getPurgeDate() != null)
//    {
//      long ttl = kvItem.getPurgeDate().toEpochMilli() / 1000;
//      
//      baseLength += ColumnNameTTL.length() + String.valueOf(ttl).length();
//      
//      item.withLong(ColumnNameTTL,       ttl);
//    }
//    
//    items.add(item);
//    
//    int length = baseLength + ColumnNameDocument.length() + kvItem.getJson().length();
//    
//    if(length < payloadLimit)
//    {
//      item.withJSON(ColumnNameDocument, kvItem.getJson());
//      return false;
//    }
//    else
//    {
//      return true;
//    }
//  }

  private String getPartitionKey(IKvPartitionKeyProvider kvItem)
  {
    return serviceId_ + Separator + kvItem.getPartitionKey();
  }

  @Override
  public void start()
  {
  }

  @Override
  public void stop()
  {
    if(amazonDynamoDB_ != null)
      amazonDynamoDB_.shutdown();
  }

//  private DynamoDbTableAdmin createTableAdmin()
//  {
//    return new DynamoDbTableAdmin(nameFactory_, objectTable_, getAmazonDynamoDB(), stsManager_)
//    {
//    
//      @Override
//      protected CreateTableRequest createCreateTableRequest()
//      {
//        return new CreateTableRequest()
//
//            .withTableName(objectTable_.getTableName())
//            .withAttributeDefinitions(
//                new AttributeDefinition(ColumnNameHashKey, ScalarAttributeType.S),
//                new AttributeDefinition(ColumnNameSortKey, ScalarAttributeType.S)
//                )
//            .withKeySchema(new KeySchemaElement(ColumnNameHashKey, KeyType.HASH), new KeySchemaElement(ColumnNameSortKey, KeyType.RANGE))
//            ;
//      }
//    }
//    .withTtlColumnName(ColumnNameTTL);
//  }
  
  @Override
  public void createTable(boolean dryRun)
  {
    HashMap tagMap = new HashMap<>(nameFactory_.getTags());
    
    tagMap.put(Fugue.TAG_FUGUE_SERVICE, serviceId_);
    tagMap.put(Fugue.TAG_FUGUE_ITEM, objectTableName_);
    
    List tags = new AwsTags(tagMap).getDynamoTags();
    
    String tableArn;
    
    try
    {
      TableDescription tableInfo = amazonDynamoDB_.describeTable(objectTableName_).getTable();

      tableArn = tableInfo.getTableArn();

      log_.info("Table \"" + objectTableName_ + "\" already exists as " + tableArn);
      
      configureStream(streamSpecification_, tableInfo);
    }
    catch (ResourceNotFoundException e)
    {
      // Table does not exist, create it
      
      if(dryRun)
      {
        log_.info("Table \"" + objectTableName_ + "\" does not exist and would be created");
        return;
      }
      else
      {
        try
        {
          CreateTableRequest    request;
          CreateTableResult     result;
          
          request = new CreateTableRequest()
              .withTableName(objectTable_.getTableName())
              .withAttributeDefinitions(
                  new AttributeDefinition(ColumnNamePartitionKey, ScalarAttributeType.S),
                  new AttributeDefinition(ColumnNameSortKey, ScalarAttributeType.S)
                  )
              .withKeySchema(new KeySchemaElement(ColumnNamePartitionKey, KeyType.HASH), new KeySchemaElement(ColumnNameSortKey, KeyType.RANGE))
              .withBillingMode(BillingMode.PAY_PER_REQUEST)
              .withStreamSpecification(streamSpecification_)
              ;
          
          result = amazonDynamoDB_.createTable(request);
          tableArn = result.getTableDescription().getTableArn();
          
          log_.info("Table \"" + objectTableName_ + "\" created as " + tableArn);
        }
        catch (RuntimeException e2)
        {
          log_.error("Failed to create tables", e2);
          throw e2;
        }
              
        try
        {
          objectTable_.waitForActive();
        }
        catch (InterruptedException e2)
        {
          throw new IllegalStateException(e2);
        }
      }
    }
    
    
//    configureAutoScale();
    
    try
    {
      DescribeTimeToLiveRequest describeTimeToLiveRequest = new DescribeTimeToLiveRequest().withTableName(objectTableName_);
      
      DescribeTimeToLiveResult ttlDescResult = amazonDynamoDB_.describeTimeToLive(describeTimeToLiveRequest);
      
      TimeToLiveDescription ttlDesc = ttlDescResult.getTimeToLiveDescription();
      
      if("ENABLED".equals(ttlDesc.getTimeToLiveStatus()))
      {
        log_.info("Table \"" + objectTableName_ + "\" already has TTL enabled.");
      }
      else
      {
        if(dryRun)
        {
          log_.info("Table \"" + objectTableName_ + "\" does not have TTL set and it would be set for column " + ColumnNameTTL);
        }
        else
        {
          //table created now enabling TTL
          UpdateTimeToLiveRequest req = new UpdateTimeToLiveRequest();
          req.setTableName(objectTableName_);
           
          TimeToLiveSpecification ttlSpec = new TimeToLiveSpecification();
          ttlSpec.setAttributeName(ColumnNameTTL);
          ttlSpec.setEnabled(true);
           
          req.withTimeToLiveSpecification(ttlSpec);
           
          UpdateTimeToLiveResult result2 = amazonDynamoDB_.updateTimeToLive(req);
          log_.info("Table \"" + objectTableName_ + "\" TTL updated " + result2);
        }
      }
    }
    catch (RuntimeException e)
    {
      log_.info("Failed to update TTL for table \"" + objectTableName_ + "\"", e);
      throw e;
    }
    
    try
    {
      amazonDynamoDB_.tagResource(new TagResourceRequest()
          .withResourceArn(tableArn)
          .withTags(tags)
          );
      log_.info("Table \"" + objectTableName_ + "\" tagged");
    }
    catch (RuntimeException e)
    {
      log_.error("Failed to add tags", e);
      throw e;
    }
    
    try
    {
      objectTable_.waitForActive();
    }
    catch (InterruptedException e)
    {
      throw new IllegalStateException(e);
    }
  }

//  private void configureAutoScale()
//  {
//    boolean updateTable = false;
//    UpdateTableRequest  updateRequest = new UpdateTableRequest()
//        .withTableName(objectTableName_);
//    
//    TableDescription tableInfo = amazonDynamoDB_.describeTable(objectTableName_).getTable();
//    
//    if(tableInfo.getBillingModeSummary() != null && BillingMode.PAY_PER_REQUEST.toString().equals(tableInfo.getBillingModeSummary().getBillingMode()))
//    {
//      log_.info("Table is set to on-demand - no change made.");
//    }
//    else
//    {
//      log_.info("Updating table to on-demand mode");
//      updateRequest.withBillingMode(BillingMode.PAY_PER_REQUEST);
//      updateTable=true;
//    }
//    
//    if(updateTable)
//    {
//      try
//      {
//        amazonDynamoDB_.updateTable(updateRequest);
//      }
//      catch(AmazonDynamoDBException e)
//      {
//        log_.error("Unable to update table throughput.", e);
//      }
//    }
//  }
  
  private void configureStream(StreamSpecification streamSpecification, TableDescription tableInfo)
  {
    String streamArn = tableInfo.getLatestStreamArn();
    
    if(streamSpecification == null || !streamSpecification.isStreamEnabled())
    {
      if(streamArn != null || (tableInfo.getStreamSpecification() != null && tableInfo.getStreamSpecification().isStreamEnabled()))
      {
        log_.info("Table has streams enabled, disabling....");
        streamSpecification = new StreamSpecification().withStreamEnabled(false);
      }
      else
      {
        log_.info("Table does not have streams enabled, nothing to do here.");
        return;
      }
    }
    else
    {
      if(streamArn == null || !tableInfo.getStreamSpecification().isStreamEnabled())
      {
        log_.info("Enabling streams for table....");
      }
      else if(!tableInfo.getStreamSpecification().getStreamViewType().equals(streamSpecification.getStreamViewType())
          && tableInfo.getStreamSpecification().getStreamEnabled()
          && streamSpecification.getStreamEnabled())
      {
        log_.info("Changing stream view type for table, ....");
        
        StreamSpecification disabled = new StreamSpecification()
            //.withStreamViewType(tableInfo.getStreamSpecification().getStreamViewType())
            .withStreamEnabled(false);
        
        UpdateTableRequest  updateTableRequest = new UpdateTableRequest()
            .withTableName(objectTable_.getTableName())
            .withStreamSpecification(disabled)
            ;
        
        amazonDynamoDB_.updateTable(updateTableRequest);
        
        log_.info("Waiting for table to be active...");
        try
        {
          objectTable_.waitForActive();
        }
        catch (InterruptedException e2)
        {
          throw new IllegalStateException(e2);
        }
        log_.info("Waiting for table to be active...DONE");
      }
      else
      {
        log_.info("Table has streams enabled, nothing to do here.");
        return;
      }
    }
    
    
    UpdateTableRequest  updateTableRequest = new UpdateTableRequest()
        .withTableName(objectTable_.getTableName())
        .withStreamSpecification(streamSpecification)
        ;
    
    amazonDynamoDB_.updateTable(updateTableRequest);
    
    log_.info("Stream settings updated.");
  }

  @Override
  public void deleteTable(boolean dryRun)
  {
    try
    {
      TableDescription tableInfo = amazonDynamoDB_.describeTable(objectTableName_).getTable();

      String tableArn = tableInfo.getTableArn();
      
      if(dryRun)
      {
        log_.info("Table \"" + objectTableName_ + "\" with arn " + tableArn + " would be deleted (dry run).");
      }
      else
      {
        log_.info("Deleting table \"" + objectTableName_ + "\" with arn " + tableArn + "...");

        amazonDynamoDB_.deleteTable(new DeleteTableRequest()
            .withTableName(objectTableName_));
      }
    }
    catch (ResourceNotFoundException e)
    {
      log_.info("Table \"" + objectTableName_ + "\" Does not exist.");
    }
  }

  @Override
  public IKvPagination fetchPartitionObjects(IKvPartitionKeyProvider partitionKey, boolean scanForwards, Integer limit, 
      @Nullable String after,
      @Nullable String sortKeyPrefix,
      @Nullable Map filterAttributes,
      Consumer consumer, ITraceContext trace)
  {
    return doFetchPartitionObjects(partitionKey, scanForwards, limit, after, sortKeyPrefix, filterAttributes, new PartitionConsumer(consumer), trace);
  }

  private IKvPagination doFetchPartitionObjects(IKvPartitionKeyProvider partitionKey, boolean scanForwards, Integer limit, 
      @Nullable String after,
      @Nullable String sortKeyPrefix,
      @Nullable Map filterAttributes,
      AbstractItemConsumer consumer, ITraceContext trace)
  {
    return doDynamoQueryTask(() ->
    {
      ValueMap valueMap = new ValueMap()
          .withString(":v_partition", getPartitionKey(partitionKey))
          ;
      
      String keyConditionExpression = ColumnNamePartitionKey + " = :v_partition";
      
      if(sortKeyPrefix != null)
      {
        keyConditionExpression += " and begins_with(" + ColumnNameSortKey + ", :v_sortKeyPrefix)";
        valueMap.put(":v_sortKeyPrefix", sortKeyPrefix);
      }
      
      StringBuilder filter = null;
      
      if(filterAttributes != null)
      {
        for(Entry entry : filterAttributes.entrySet())
        {
          if(filter == null)
            filter = new StringBuilder();
          else
            filter.append(" and ");
          
          filter.append(entry.getKey());
          filter.append(" = :f_" );
          filter.append(entry.getKey());
          valueMap.put(":f_" + entry.getKey(), entry.getValue());
        }
      }
      
      QuerySpec spec = new QuerySpec()
          .withKeyConditionExpression(keyConditionExpression)
          .withValueMap(valueMap)
          .withScanIndexForward(scanForwards)
          ;
      
      if(filter != null)
      {
        spec.withFilterExpression(filter.toString());
      }
      
      if(limit != null)
      {
        spec.withMaxResultSize(limit);
      }
      
      if(after != null && after.length()>0)
      {
        spec.withExclusiveStartKey(
            new KeyAttribute(ColumnNamePartitionKey, getPartitionKey(partitionKey)),
            new KeyAttribute(ColumnNameSortKey,  after)
            );
      }
    
      Map lastEvaluatedKey = null;
      ItemCollection items = objectTable_.query(spec);
      String before = null;
      for(Page page : items.pages())
      {
        Iterator it = page.iterator();
        
        while(it.hasNext())
        {
          Item item = it.next();
          
          consumer.consume(item, trace);
          
          if(before == null && after != null)
          {
            before = item.getString(ColumnNameSortKey);
          }
        }
      }
      
      if(before == null && after != null)
      {
        before = "";
      }
      
      lastEvaluatedKey = items.getLastLowLevelResult().getQueryResult().getLastEvaluatedKey();
      
      if(lastEvaluatedKey != null)
      {
        AttributeValue sequenceKeyAttr = lastEvaluatedKey.get(ColumnNameSortKey);
        
        return new KvPagination(before, sequenceKeyAttr.getS());
      }
      
      return new KvPagination(before, null);
    });
  }

  abstract class AbstractItemConsumer
  {
    abstract void consume(Item item, ITraceContext trace);
  }
  
  class PartitionConsumer extends AbstractItemConsumer
  {
    Consumer consumer_;
    
    PartitionConsumer(Consumer consumer)
    {
      consumer_ = consumer;
    }

    @Override
    void consume(Item item, ITraceContext trace)
    {
      String payloadString = item.getString(ColumnNameDocument);
      
      if(payloadString == null)
      {
        String hashString = item.getString(ColumnNameAbsoluteHash);
        Hash absoluteHash = Hash.newInstance(hashString);
        
        try
        {
          payloadString = fetchFromSecondaryStorage(absoluteHash, trace);
        }
        catch (NoSuchObjectException e)
        {
          throw new IllegalStateException("Unable to read known object from S3", e);
        }
      }
      
      consumer_.accept(payloadString);
    }
  }

  protected static abstract class AbstractBuilder, B extends AbstractDynamoDbKvTable> extends AbstractKvTable.AbstractBuilder
  {
    protected final AmazonDynamoDBClientBuilder amazonDynamoDBClientBuilder_;

    protected String              region_;
    protected int                 payloadLimit_           = MAX_RECORD_SIZE;
    protected boolean             validate_               = true;
    protected boolean             enableSecondaryStorage_ = false;

    protected StreamSpecification streamSpecification_    = new StreamSpecification().withStreamEnabled(false);
    
    protected AbstractBuilder(Class type)
    {
      super(type);
      
      amazonDynamoDBClientBuilder_ = AmazonDynamoDBClientBuilder.standard();
    }
    
    @Override
    public void validate(FaultAccumulator faultAccumulator)
    {
      super.validate(faultAccumulator);
      
      faultAccumulator.checkNotNull(region_,      "region");
    }

    public T withValidate(boolean validate)
    {
      validate_ = validate;
      
      return self();
    }
    
    public T withStreamSpecification(StreamSpecification streamSpecification)
    {
      streamSpecification_ = streamSpecification;
      
      return self();
    }

    public T withEnableSecondaryStorage(boolean enableSecondaryStorage)
    {
      enableSecondaryStorage_ = enableSecondaryStorage;
      
      return self();
    }

    public T withRegion(String region)
    {
      region_ = region;
      
      return self();
    }

    public T withPayloadLimit(int payloadLimit)
    {
      payloadLimit_ = Math.min(payloadLimit, MAX_RECORD_SIZE);
      
      return self();
    }

    public T withCredentials(AWSCredentialsProvider credentials)
    {
      amazonDynamoDBClientBuilder_.withCredentials(credentials);
      
      return self();
    }
  }
}