Package com.elasticinbox.core.cassandra

Source Code of com.elasticinbox.core.cassandra.CassandraMessageDAO$MessageAggregator

/**
* Copyright (c) 2011-2013 Optimax Software Ltd.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
*  * Redistributions of source code must retain the above copyright notice,
*    this list of conditions and the following disclaimer.
*  * Redistributions in binary form must reproduce the above copyright notice,
*    this list of conditions and the following disclaimer in the documentation
*    and/or other materials provided with the distribution.
*  * Neither the name of Optimax Software, ElasticInbox, nor the names
*    of its contributors may be used to endorse or promote products derived
*    from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package com.elasticinbox.core.cassandra;

import static me.prettyprint.hector.api.factory.HFactory.createMutator;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import me.prettyprint.cassandra.serializers.StringSerializer;
import me.prettyprint.cassandra.utils.TimeUUIDUtils;
import me.prettyprint.hector.api.Keyspace;
import me.prettyprint.hector.api.mutation.Mutator;

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

import com.elasticinbox.config.Configurator;
import com.elasticinbox.core.IllegalLabelException;
import com.elasticinbox.core.MessageDAO;
import com.elasticinbox.core.MessageModification;
import com.elasticinbox.core.OverQuotaException;
import com.elasticinbox.core.blob.BlobDataSource;
import com.elasticinbox.core.blob.compression.CompressionHandler;
import com.elasticinbox.core.blob.compression.DeflateCompressionHandler;
import com.elasticinbox.core.blob.encryption.AESEncryptionHandler;
import com.elasticinbox.core.blob.encryption.EncryptionHandler;
import com.elasticinbox.core.blob.store.BlobStorage;
import com.elasticinbox.core.blob.store.BlobStorageMediator;
import com.elasticinbox.core.cassandra.persistence.*;
import com.elasticinbox.core.cassandra.utils.BatchConstants;
import com.elasticinbox.core.cassandra.utils.ThrottlingMutator;
import com.elasticinbox.core.model.Label;
import com.elasticinbox.core.model.LabelCounters;
import com.elasticinbox.core.model.LabelMap;
import com.elasticinbox.core.model.Mailbox;
import com.elasticinbox.core.model.Marker;
import com.elasticinbox.core.model.Message;
import com.elasticinbox.core.model.ReservedLabels;
import com.google.common.collect.Lists;

public final class CassandraMessageDAO extends AbstractMessageDAO implements MessageDAO
{
  private final Keyspace keyspace;
  private final static StringSerializer strSe = StringSerializer.get();
 
  private final BlobStorage blobStorage;

  private final static Logger logger =
      LoggerFactory.getLogger(CassandraMessageDAO.class);
 
  public CassandraMessageDAO(Keyspace keyspace)
  {
    this.keyspace = keyspace;
   
    // Create BlobStorage instance with AES encryption and Deflate compression
    CompressionHandler compressionHandler =
        Configurator.isBlobStoreCompressionEnabled() ? new DeflateCompressionHandler() : null;
    EncryptionHandler encryptionHandler =
        Configurator.isBlobStoreEncryptionEnabled() ? new AESEncryptionHandler() : null;

    this.blobStorage = new BlobStorageMediator(compressionHandler, encryptionHandler);
  }

  @Override
  public Message getParsed(final Mailbox mailbox, final UUID messageId)
  {
    return MessagePersistence.fetch(mailbox.getId(), messageId, true);
  }

  @Override
  public BlobDataSource getRaw(final Mailbox mailbox, final UUID messageId)
      throws IOException
  {
    Message metadata = MessagePersistence.fetch(mailbox.getId(), messageId, false);
    return blobStorage.read(metadata.getLocation());
  }

  @Override
  public Map<UUID, Message> getMessageIdsWithMetadata(final Mailbox mailbox,
      final int labelId, final UUID start, final int count, boolean reverse, boolean includeBody)
  {
    List<UUID> messageIds =
        getMessageIds(mailbox, labelId, start, count, reverse);

    return MessagePersistence.fetch(mailbox.getId(), messageIds, includeBody);
  }

  @Override
  public List<UUID> getMessageIds(final Mailbox mailbox, final int labelId,
      final UUID start, final int count, final boolean reverse)
  {
    return LabelIndexPersistence.get(mailbox.getId(), labelId, start, count, reverse);
  }

  @Override
  public void put(final Mailbox mailbox, UUID messageId, Message message, InputStream in)
      throws IOException, OverQuotaException
  {
    URI uri = null;
    logger.debug("Storing message: key={}", messageId.toString());

    // Check quota
    LabelCounters mailboxCounters = LabelCounterPersistence.get(
        mailbox.getId(), ReservedLabels.ALL_MAILS.getId());

    long requiredBytes = mailboxCounters.getTotalBytes() + message.getSize();
    long requiredCount = mailboxCounters.getTotalMessages() + 1;

    if ((requiredBytes > Configurator.getDefaultQuotaBytes()) ||
      (requiredCount > Configurator.getDefaultQuotaCount()))
    {
      logger.info("Mailbox is over quota: {} size={}/{}, count={}/{}",
          new Object[] { mailbox.getId(), requiredBytes,
              Configurator.getDefaultQuotaBytes(), requiredCount,
              Configurator.getDefaultQuotaCount() });

      throw new OverQuotaException("Mailbox is over quota");
    }

    // Order is important, add to label after message written

    // store blob
    if (in != null)
    {
      try {
        uri = blobStorage.write(messageId, mailbox,
            Configurator.getBlobStoreWriteProfileName(), in, message.getSize())
            .buildURI();

        // update location in metadata
        message.setLocation(uri);
      } catch (Exception e) {
        throw new IOException("Failed to store blob: ", e);
      } finally {
        if (in != null) {
          in.close();
        }
      }
    }

    // automatically add "all" label to all new messages
    message.addLabel(ReservedLabels.ALL_MAILS.getId());

    try {
      // begin batch operation
      Mutator<String> m = createMutator(keyspace, strSe);

      // store metadata
      MessagePersistence.persistMessage(m, mailbox.getId(), messageId, message);
      // add indexes
      LabelIndexPersistence.add(m, mailbox.getId(), messageId, message.getLabels());
      // update counters
      LabelCounterPersistence.add(m, mailbox.getId(), message.getLabels(), message.getLabelCounters());

      // commit batch operation
      m.execute();
    } catch (Exception e) {
      logger.warn(
          "Unable to store metadata for message {}, deleting blob {}",
          messageId, uri);

      // rollback
      if (uri != null) {
        blobStorage.delete(uri);
      }

      throw new IOException("Unable to store message metadata: ", e);
    }
  }
 
  @Override
  public void modify(Mailbox mailbox, List<UUID> messageIds, MessageModification mod)
  {
    // label "all" cannot be removed from message
    if (mod.getLabelsToRemove().contains(ReservedLabels.ALL_MAILS.getId())) {
      throw new IllegalLabelException("This label cannot be removed");
    }

    // begin batch operation
    ThrottlingMutator<String> mutator = new ThrottlingMutator<String>(keyspace, strSe,
        BatchConstants.BATCH_WRITES, BatchConstants.BATCH_WRITE_INTERVAL);

    // prepare message attributes
    Set<String> labelsToAddAsAttributes = labelsToMessageAttibutes(mod.getLabelsToAdd());
    Set<String> labelsToRemoveAsAttributes = labelsToMessageAttibutes(mod.getLabelsToRemove());
    Set<String> markersToAddAsAttributes = markersToMessageAttibutes(mod.getMarkersToAdd());
    Set<String> markersToRemoveAsAttributes = markersToMessageAttibutes(mod.getMarkersToRemove());

    // get message stats for counters
    MessageAggregator ma = new MessageAggregator(mailbox, messageIds);

    for (UUID messageId : ma.getValidMessageIds())
    {
      Message message = ma.getMessage(messageId);

      // add labels
      if (!mod.getLabelsToAdd().isEmpty())
      {
        // add labels to messages
        MessagePersistence.persistAttributes(mutator, mailbox.getId(), messageId, labelsToAddAsAttributes);

        // add messages to label index
        LabelIndexPersistence.add(mutator, mailbox.getId(), messageId, mod.getLabelsToAdd());

        // increment label counters
        for (int labelId : mod.getLabelsToAdd())
        {
          // count only if message does not have label
          if (!message.getLabels().contains(labelId)) {
            LabelCounterPersistence.add(mutator, mailbox.getId(), labelId, message.getLabelCounters());
          }
        }
      }

      // remove labels
      if (!mod.getLabelsToRemove().isEmpty())
      {
        // remove labels from messages
        MessagePersistence.deleteAttributes(mutator, mailbox.getId(), messageId, labelsToRemoveAsAttributes);

        // remove messages from label index
        LabelIndexPersistence.remove(mutator, mailbox.getId(), messageId, mod.getLabelsToRemove());

        // decrement label counters
        for (int labelId : mod.getLabelsToRemove())
        {
          // count only if message had label
          if (message.getLabels().contains(labelId)) {
            LabelCounterPersistence.subtract(mutator, mailbox.getId(), labelId, message.getLabelCounters());
          }
        }
      }

      // add markers
      if (!mod.getMarkersToAdd().isEmpty())
      {
        // add markers to messages
        MessagePersistence.persistAttributes(mutator, mailbox.getId(), messageIds, markersToAddAsAttributes);

        // decrement unread message counter only if message does not have SEEN marker
        if (mod.getMarkersToAdd().contains(Marker.SEEN) && !message.getMarkers().contains(Marker.SEEN))
        {
          LabelCounters labelCounters = new LabelCounters();
          labelCounters.setUnreadMessages(1L);
          LabelCounterPersistence.subtract(mutator, mailbox.getId(), message.getLabels(), labelCounters);
        }
      }
     
      // remove markers
      if (!mod.getMarkersToRemove().isEmpty())
      {
        // remove markers from messages
        MessagePersistence.deleteAttributes(mutator, mailbox.getId(), messageIds, markersToRemoveAsAttributes);

        // increment unread message counter only if message has SEEN marker
        if (mod.getMarkersToRemove().contains(Marker.SEEN) && message.getMarkers().contains(Marker.SEEN))
        {
          LabelCounters labelCounters = new LabelCounters();
          labelCounters.setUnreadMessages(1L);
          LabelCounterPersistence.add(mutator, mailbox.getId(), message.getLabels(), labelCounters);
        }
      }

      mutator.executeIfFull();
    }

    mutator.execute();
  }

  @Override
  public void delete(final Mailbox mailbox, final List<UUID> messageIds)
  {
    // begin batch operation
    ThrottlingMutator<String> mutator = new ThrottlingMutator<String>(keyspace, strSe,
        BatchConstants.BATCH_WRITES, BatchConstants.BATCH_WRITE_INTERVAL);
   
    // READ:WRITE ration is 1:5
    final int readBatchSize = BatchConstants.BATCH_WRITES / 5;

    for (List<UUID> idSubList : Lists.partition(messageIds, readBatchSize))
    {
      // get label stats
      MessageAggregator ma = new MessageAggregator(mailbox, idSubList);
      LabelMap labels = ma.aggregateCountersByLabel();

      // validate message ids
      List<UUID> validMessageIds = new ArrayList<UUID>(ma.getValidMessageIds());
      List<UUID> invalidMessageIds = new ArrayList<UUID>(ma.getInvalidMessageIds());

      // add only valid messages to purge index
      PurgeIndexPersistence.add(mutator, mailbox.getId(), validMessageIds);

      // remove valid message ids from label indexes, including "all"
      LabelIndexPersistence.remove(mutator, mailbox.getId(), validMessageIds, labels.getIds());

      // decrement label counters (add negative value)
      for (Integer labelId : labels.getIds()) {
        LabelCounterPersistence.subtract(mutator, mailbox.getId(), labelId, labels.get(labelId).getCounters());
      }

      // remove invalid message ids from all known labels
      LabelMap allLabels = AccountPersistence.getLabels(mailbox.getId());
      LabelIndexPersistence.remove(mutator, mailbox.getId(), invalidMessageIds, allLabels.getIds());

      // signal end of batch
      mutator.executeIfFull();
    }

    // commit batch operation
    mutator.execute();
  }

  @Override
  public void purge(final Mailbox mailbox, final Date age) throws IOException
  {
    Map<UUID, UUID> purgeIndex = null;

    logger.debug("Purging all messages older than {} for {}", age.toString(), mailbox);

    // initiate throttling mutator
    ThrottlingMutator<String> mutator = new ThrottlingMutator<String>(keyspace, strSe,
        BatchConstants.BATCH_WRITES, BatchConstants.BATCH_WRITE_INTERVAL);

    // READ:WRITE ratio is 1:2
    final int readBatchSize = BatchConstants.BATCH_WRITES / 2;

    // loop until we process all purged items
    do {
      // get message IDs of messages to purge
      purgeIndex = PurgeIndexPersistence.get(mailbox.getId(), age, readBatchSize);

      // get metadata/blob location
      Map<UUID, Message> messages =
          MessagePersistence.fetch(mailbox.getId(), purgeIndex.values(), false);

      // delete message sources from object store
      for(UUID messageId : messages.keySet()) {
        blobStorage.delete(messages.get(messageId).getLocation());
      }

      // purge expired (older than age) messages
      MessagePersistence.deleteMessage(mutator, mailbox.getId(), purgeIndex.values());

      // remove from purge index
      PurgeIndexPersistence.remove(mutator, mailbox.getId(), purgeIndex.keySet());
     
      // signal end of batch
      mutator.executeIfFull();
    }
    while (purgeIndex.size() >= readBatchSize);

    // commit remaining items
    mutator.execute();
  }

  @Override
  public LabelMap scrub(final Mailbox mailbox, final boolean rebuildIndex)
  {
    LabelMap labels = new LabelMap();
    Map<UUID, Message> messages;
    Set<UUID> purgePendingMessages = new HashSet<UUID>();
   
    // initiate throttling mutator
    ThrottlingMutator<String> mutator = new ThrottlingMutator<String>(keyspace, strSe,
        BatchConstants.BATCH_WRITES, BatchConstants.BATCH_WRITE_INTERVAL);
   
    logger.debug("Recalculating counters for {}", mailbox);

    // Get message IDs pending purge. Such messages should be excluded during calculation.
    purgePendingMessages = PurgeIndexPersistence.getAll(mailbox.getId());

    logger.debug("Found {} messages pending purge. Will exclude them from calculations.", purgePendingMessages.size());

    UUID start = TimeUUIDUtils.getUniqueTimeUUIDinMillis();
    do {
      // reset start, read messages and calculate label counters
      messages = MessagePersistence.getRange(
          mailbox.getId(), start, BatchConstants.BATCH_READS);

      for (UUID messageId : messages.keySet())
      {
        start = messageId; // shift next query start

        // skip messages from purge queue
        if (purgePendingMessages.contains(messageId)) continue;

        Message message = messages.get(messageId);

        // add counters for each of the labels
        for (int labelId : message.getLabels())
        {
          if (!labels.containsId(labelId)) {
            Label label = new Label(labelId).setCounters(message.getLabelCounters());
            labels.put(label);
          } else {
            labels.get(labelId).incrementCounters(message.getLabelCounters());
          }

          if (rebuildIndex)
          {
            // add message ID to the label index
            LabelIndexPersistence.add(mutator, mailbox.getId(), messageId, labelId);
            mutator.executeIfFull();
          }
        }

        logger.debug("Counters state after message {} is {}", messageId, labels.toString());
      }
     
    }
    while (messages.size() >= BatchConstants.BATCH_READS);

    // commit remaining items
    mutator.execute();

    return labels;
  }

  /**
   * Convert label IDs to message attributes.
   * 
   * @param labelIds
   * @return
   */
  private static Set<String> labelsToMessageAttibutes(Set<Integer> labelIds)
  {
    Set<String> attributes = new HashSet<String>(labelIds.size());
    for (Integer labelId : labelIds) {
      attributes.add(Marshaller.CN_LABEL_PREFIX + labelId);
    }

    return attributes;
  }

  /**
   * Convert markers to message attributes.
   * 
   * @param labelIds
   * @return
   */
  private static Set<String> markersToMessageAttibutes(Set<Marker> markers)
  {
    Set<String> attributes = new HashSet<String>(markers.size());
    for (Marker marker : markers)
    {
      String a = new StringBuilder(Marshaller.CN_MARKER_PREFIX)
          .append(marker.toInt()).toString();
      attributes.add(a);
    }

    return attributes;
  }

  /**
   * Aggregate messages to provide stats
   */
  private class MessageAggregator
  {
    private final Map<UUID, Message> messages;
    private final HashSet<UUID> invalidMessageIds;

    public MessageAggregator(final Mailbox mailbox, final List<UUID> messageIds)
    {
      // get message headers
      messages = MessagePersistence.fetch(mailbox.getId(), messageIds, false);

      invalidMessageIds = new HashSet<UUID>(messageIds);
      invalidMessageIds.removeAll(this.getValidMessageIds());
    }

    /**
     * Get message
     *
     * @param messageId
     * @return
     */
    public Message getMessage(UUID messageId)
    {
      return messages.get(messageId);
    }

    /**
     * Get aggregated {@link LabelCounter} stats for each label in the list of
     * messages. Results aggregated by label ID.
     *
     * @return
     */
    public LabelMap aggregateCountersByLabel()
    {
      LabelMap labels = new LabelMap();

      // get all labels of all messages, including label "all"
      for (UUID messageId : this.messages.keySet())
      {
        Set<Integer> messageLabels = this.messages.get(messageId).getLabels();
 
        for (int labelId : messageLabels)
        {
          if (!labels.containsId(labelId)) {
            Label label = new Label(labelId).
                setCounters(this.messages.get(messageId).getLabelCounters());
            labels.put(label);
          } else {
            labels.get(labelId).getCounters().add(
                this.messages.get(messageId).getLabelCounters());
          }
        }
      }

      return labels;
    }

    /**
     * Returns message IDs which exist in message metadata.
     *
     * In some cases, message can be deleted from metadata but not from
     * index. Use this method to filter out such messages.
     *
     * @return
     */
    public Set<UUID> getValidMessageIds() {
      return messages.keySet();
    }

    /**
     * Returns message IDs which do not exist in message metadata.
     *
     * @return
     */
    public Set<UUID> getInvalidMessageIds() {
      return invalidMessageIds;
    }
  }
}
TOP

Related Classes of com.elasticinbox.core.cassandra.CassandraMessageDAO$MessageAggregator

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.