/* * Copyright © 2014 Cask Data, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package co.cask.cdap.data2.transaction.queue.inmemory; import co.cask.cdap.common.utils.ImmutablePair; import co.cask.cdap.data2.queue.ConsumerConfig; import co.cask.cdap.data2.queue.DequeueStrategy; import co.cask.cdap.data2.queue.QueueEntry; import co.cask.cdap.data2.transaction.queue.ConsumerEntryState; import co.cask.tephra.Transaction; import com.google.common.base.Objects; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; import java.util.NavigableSet; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentNavigableMap; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.atomic.AtomicInteger; /** * Implementation of an in-memory queue. */ public class InMemoryQueue { private static final Logger LOG = LoggerFactory.getLogger(InMemoryQueue.class); private final ConcurrentNavigableMap<Key, Item> entries = new ConcurrentSkipListMap<>(); public void clear() { entries.clear(); } public int getSize() { return entries.size(); } public void enqueue(long txId, int seqId, QueueEntry entry) { entries.put(new Key(txId, seqId), new Item(entry)); } public void undoEnqueue(long txId, int seqId) { entries.remove(new Key(txId, seqId)); } public ImmutablePair<List<Key>, List<byte[]>> dequeue(Transaction tx, ConsumerConfig config, ConsumerState consumerState, int maxBatchSize) { List<Key> keys = Lists.newArrayListWithCapacity(maxBatchSize); List<byte[]> datas = Lists.newArrayListWithCapacity(maxBatchSize); NavigableSet<Key> keysToScan = consumerState.startKey == null ? entries.navigableKeySet() : entries.tailMap(consumerState.startKey).navigableKeySet(); boolean updateStartKey = true; // navigableKeySet is immune to concurrent modification for (Key key : keysToScan) { if (keys.size() >= maxBatchSize) { break; } if (updateStartKey && key.txId < tx.getFirstShortInProgress()) { // See QueueEntryRow#canCommit for reason. consumerState.startKey = key; } if (tx.getReadPointer() < key.txId) { // the entry is newer than the current transaction. so are all subsequent entries. bail out. break; } else if (tx.isInProgress(key.txId)) { // the entry is in the exclude list of current transaction. There is a chance that visible entries follow. updateStartKey = false; // next time we have to revisit this entry continue; } Item item = entries.get(key); if (item == null) { // entry was deleted (evicted or undone) after we started iterating continue; } // check whether this is processed already ConsumerEntryState state = item.getConsumerState(config.getGroupId()); if (ConsumerEntryState.PROCESSED.equals(state)) { // already processed but not yet evicted. move on continue; } if (config.getDequeueStrategy().equals(DequeueStrategy.FIFO)) { // for FIFO, attempt to claim the entry and return it if (item.claim(config)) { keys.add(key); datas.add(item.entry.getData()); } // else: someone else claimed it, or it was already processed, move on, but we may have to revisit this. updateStartKey = false; continue; } // for hash/round robin, if group size is 1, just take it if (config.getGroupSize() == 1) { keys.add(key); datas.add(item.entry.getData()); updateStartKey = false; continue; } // hash by entry hash key or entry id int hash; if (config.getDequeueStrategy().equals(DequeueStrategy.ROUND_ROBIN)) { hash = key.hashCode(); } else { Integer hashFoundInEntry = item.entry.getHashKey(config.getHashKey()); hash = hashFoundInEntry == null ? 0 : hashFoundInEntry; } // modulo of a negative is negative, make sure we're positive or 0. if (Math.abs(hash) % config.getGroupSize() == config.getInstanceId()) { keys.add(key); datas.add(item.entry.getData()); updateStartKey = false; } } return keys.isEmpty() ? null : ImmutablePair.of(keys, datas); } public void ack(List<Key> dequeuedKeys, ConsumerConfig config) { if (dequeuedKeys == null) { return; } for (Key key : dequeuedKeys) { Item item = entries.get(key); if (item == null) { LOG.warn("Attempting to ack non-existing entry " + key); continue; } item.setConsumerState(config, ConsumerEntryState.PROCESSED); } } public void undoDequeue(List<Key> dequeuedKeys, ConsumerConfig config) { if (dequeuedKeys == null) { return; } for (Key key : dequeuedKeys) { Item item = entries.get(key); if (item == null) { LOG.warn("Attempting to undo dequeue for non-existing entry " + key); continue; } item.revokeConsumerState(config, config.getDequeueStrategy() == DequeueStrategy.FIFO); } } public void evict(List<Key> dequeuedKeys, int numGroups) { if (numGroups < 1) { return; // this means no eviction because number of groups is not known } if (dequeuedKeys == null) { return; } for (Key key : dequeuedKeys) { Item item = entries.get(key); if (item == null) { LOG.warn("Attempting to evict non-existing entry " + key); continue; } if (item.incrementProcessed() >= numGroups) { // all consumer groups have processed _and_ reached the post-commit hook: safe to evict entries.remove(key); } } } /** * Used as the key of each queue item, composed of a transaction id and a sequence number within the transaction. */ public static final class Key implements Comparable<Key> { final long txId; final int seqNo; Key(long tx, int seq) { txId = tx; seqNo = seq; } public boolean equals(Object obj) { if (obj == this) { return true; } if (obj == null || obj.getClass() != Key.class) { return false; } Key other = (Key) obj; return txId == other.txId && seqNo == other.seqNo; } @Override public int hashCode() { // return ((int) (txId >> 32)) ^ ((int) txId) ^ seqNo; return Objects.hashCode(txId, seqNo); } @Override public int compareTo(Key o) { if (txId == o.txId) { return seqNo == o.seqNo ? 0 : seqNo < o.seqNo ? -1 : 1; } else { return txId < o.txId ? -1 : 1; } } @Override public String toString() { return txId + ":" + seqNo; } } // represents an entry of the queue plus meta data private static final class Item { final QueueEntry entry; // ConcurrentMap<Long, ConsumerEntryState> consumerStates = Maps.newConcurrentMap(); ConcurrentMap<Long, ItemEntryState> consumerStates = Maps.newConcurrentMap(); AtomicInteger processedCount = new AtomicInteger(); Item(QueueEntry entry) { this.entry = entry; } ConsumerEntryState getConsumerState(long consumerGroupId) { ItemEntryState entryState = consumerStates.get(consumerGroupId); return entryState == null ? null : entryState.getState(); } void setConsumerState(ConsumerConfig config, ConsumerEntryState newState) { consumerStates.put(config.getGroupId(), new ItemEntryState(config.getInstanceId(), newState)); } void revokeConsumerState(ConsumerConfig config, boolean revokeToClaim) { if (revokeToClaim) { consumerStates.put(config.getGroupId(), new ItemEntryState(config.getInstanceId(), ConsumerEntryState.CLAIMED)); } else { consumerStates.remove(config.getGroupId()); } } boolean claim(ConsumerConfig config) { ItemEntryState state = consumerStates.get(config.getGroupId()); if (state == null) { state = consumerStates.putIfAbsent(config.getGroupId(), new ItemEntryState(config.getInstanceId(), ConsumerEntryState.CLAIMED)); if (state == null) { return true; } } // If the old claimed consumer is gone or if it has been claimed by the same consumer before, // then it can be claimed. return state.getInstanceId() >= config.getGroupSize() || (state.getInstanceId() == config.getInstanceId() && state.getState() == ConsumerEntryState.CLAIMED); } int incrementProcessed() { return processedCount.incrementAndGet(); } } /** * Represents the state of an item entry. */ private static final class ItemEntryState { final int instanceId; ConsumerEntryState state; ItemEntryState(int instanceId, ConsumerEntryState state) { this.instanceId = instanceId; this.state = state; } int getInstanceId() { return instanceId; } ConsumerEntryState getState() { return state; } void setState(ConsumerEntryState state) { this.state = state; } } /** * The state of a single consumer, gets modified. */ public static class ConsumerState { Key startKey = null; } }