/* * Copyright (c) 2013-2017 Cinchapi 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 com.cinchapi.concourse.server.storage.db; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import com.cinchapi.concourse.annotate.PackagePrivate; import com.cinchapi.concourse.server.io.Byteable; import com.cinchapi.concourse.server.model.PrimaryKey; import com.cinchapi.concourse.server.model.Text; import com.cinchapi.concourse.server.model.Value; import com.cinchapi.concourse.server.storage.Action; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; /** * A wrapper around a collection of Revisions that provides in-memory indices to * allow efficient reads. All the Revisions in the Record must have the same * locator. They must also have the same key if the Revision is partial. * * @author Jeff Nelson * @param <L> - the locator type * @param <K> - the key type * @param <V> - value type */ @PackagePrivate @ThreadSafe @SuppressWarnings("unchecked") abstract class Record<L extends Byteable & Comparable<L>, K extends Byteable & Comparable<K>, V extends Byteable & Comparable<V>> { /** * Return a PrimaryRecord for {@code primaryKey}. * * @param primaryKey * @return the PrimaryRecord */ public static PrimaryRecord createPrimaryRecord(PrimaryKey record) { return new PrimaryRecord(record, null); } /** * Return a partial PrimaryRecord for {@code key} in {@record}. * * @param primaryKey * @param key * @return the PrimaryRecord. */ public static PrimaryRecord createPrimaryRecordPartial(PrimaryKey record, Text key) { return new PrimaryRecord(record, key); } /** * Return a SearchRecord for {@code key}. * * @param key * @return the SearchRecord */ public static SearchRecord createSearchRecord(Text key) { return new SearchRecord(key, null); } /** * Return a partial SearchRecord for {@code term} in {@code key}. * * @param key * @param term * @return the partial SearchRecord */ public static SearchRecord createSearchRecordPartial(Text key, Text term) { return new SearchRecord(key, term); } /** * Return a SeconaryRecord for {@code key}. * * @param key * @return the SecondaryRecord */ public static SecondaryRecord createSecondaryRecord(Text key) { return new SecondaryRecord(key, null); } /** * Return a partial SecondaryRecord for {@code value} in {@code key}. * * @param key * @param value * @return the SecondaryRecord */ public static SecondaryRecord createSecondaryRecordPartial(Text key, Value value) { return new SecondaryRecord(key, value); } /** * The master lock for {@link #write} and {@link #read}. DO NOT use this * lock directly. */ private final ReentrantReadWriteLock master = new ReentrantReadWriteLock(); /** * An exclusive lock that permits only one writer and no reader. Use this * lock to ensure that no read occurs while data is being appended to the * Record. */ private final WriteLock write = master.writeLock(); /** * A shared lock that permits many readers and no writer. Use this lock to * ensure that no data append occurs while a read is happening within the * Record. */ protected final ReadLock read = master.readLock(); /** * The index is used to efficiently determine the set of values currently * mapped from a key. The subclass should specify the appropriate type of * key sorting via the returned type for {@link #mapType()}. */ protected final transient Map<K, Set<V>> present = mapType(); /** * This index is used to efficiently handle historical reads. Given a * revision (e.g key/value pair), and historical timestamp, we can count the * number of times that the value appears <em>beforehand</em> at determine * if the mapping existed or not. */ protected final transient HashMap<K, List<CompactRevision<V>>> history = Maps .newHashMap(); /** * The version of the Record's most recently appended {@link Revision}. */ private transient long version = 0; /** * The locator used to identify this Record. */ protected final L locator; /** * The key used to identify this Record. This value is {@code null} unless * {@link #partial} equals {@code true}. */ @Nullable private final K key; /** * Indicates that this Record is partial and only contains Revisions for a * specific {@link #key}. */ private final boolean partial; /** * This set is returned when a key does not map to any values so that the * caller can transparently interact without performing checks or * compromising data consisentcy. This is a member variable (as opposed to * static constant) that is mocked in the constructor because it has a * generic type argument. */ private final Set<V> emptyValues = new EmptyValueSet(); /** * Construct a new instance. * * @param locator * @param key */ protected Record(L locator, @Nullable K key) { this.locator = locator; this.key = key; this.partial = key == null ? false : true; } /** * Append {@code revision} to the record by updating the in-memory indices. * The {@code revision} must have: * <ul> * <li>a higher version than that of this Record</li> * <li>a locator equal to that of this Record</li> * <li>a key equal to that of this Record if this Record is partial</li> * </ul> * * @param revision */ public void append(Revision<L, K, V> revision) { write.lock(); try { // NOTE: We only need to enforce the monotonic increasing constraint // for PrimaryRecords because Secondary and Search records will be // populated from Blocks that were sorted based primarily on // non-version factors. Preconditions .checkArgument((this instanceof PrimaryRecord && revision .getVersion() >= version) || true, "Cannot " + "append %s because its version(%s) is lower " + "than the Record's current version(%s). The", revision, revision.getVersion(), version); Preconditions.checkArgument(revision.getLocator().equals(locator), "Cannot append %s because it does not belong to %s", revision, this); // NOTE: The check below is ignored for a partial SearchRecord // instance because they 'key' is the entire search query, but we // append Revisions for each term in the query Preconditions.checkArgument( (partial && revision.getKey().equals(key)) || !partial || this instanceof SearchRecord, "Cannot append %s because it does not belong to %s", revision, this); // NOTE: The check below is ignored for a SearchRecord instance // because it will legitimately appear that "duplicate" data has // been added if similar data is added to the same key in a record // at different times (i.e. adding John Doe and Johnny Doe to the // "name") Preconditions.checkArgument(this instanceof SearchRecord || isOffset(revision), "Cannot append " + "%s because it represents an action " + "involving a key, value and locator that has not " + "been offset.", revision); // Update present index Set<V> values = present.get(revision.getKey()); if(values == null) { values = Sets.<V> newLinkedHashSet(); present.put(revision.getKey(), values); } if(revision.getType() == Action.ADD) { values.add(revision.getValue()); } else { values.remove(revision.getValue()); if(values.isEmpty()) { present.remove(revision.getKey()); } } // Update history index List<CompactRevision<V>> revisions = history.get(revision.getKey()); if(revisions == null) { revisions = Lists.newArrayList(); history.put(revision.getKey(), revisions); } revisions.add(revision.compact()); // Update metadata version = Math.max(version, revision.getVersion()); // Make revision eligible for GC revision = null; } finally { write.unlock(); } } @Override public boolean equals(Object obj) { if(obj.getClass() == this.getClass()) { Record<L, K, V> other = (Record<L, K, V>) obj; return locator.equals(other.locator) && (partial ? key.equals(other.key) : true); } return false; } /** * Return the Record's version, which is equal to the largest version of an * appended Revision. * * @return the version */ public long getVersion() { return version; } @Override public int hashCode() { return partial ? Objects.hash(locator, key) : locator.hashCode(); } /** * Return {@code true} if this record is empty and contains no present or * historical data. * * @return {@code true} if the record is empty */ public boolean isEmpty() { read.lock(); try { return present.isEmpty() && history.isEmpty(); } finally { read.unlock(); } } /** * Return {@code true} if this record is partial. * * @return {@link #partial} */ public boolean isPartial() { return partial; } @Override public String toString() { return getClass().getSimpleName() + " " + (partial ? key + " IN " : "") + locator; } /** * Return the Set of {@code keys} that map to fields which * <em>currently</em> contain values. * * @return the Set of non-empty field keys */ protected Set<K> describe() { read.lock(); try { return Collections.unmodifiableSet(present.keySet()); /* Authorized */ } finally { read.unlock(); } } /** * Return the Set of {@code keys} that mapped to fields which contained * values at {@code timestamp}. * * @param timestamp * @return the Set of non-empty field keys */ protected Set<K> describe(long timestamp) { read.lock(); try { Set<K> description = Sets.newLinkedHashSet(); Iterator<K> it = history.keySet().iterator(); /* Authorized */ while (it.hasNext()) { K key = it.next(); if(!get(key, timestamp).isEmpty()) { description.add(key); } } return description; } finally { read.unlock(); } } /** * Lazily retrieve an unmodifiable view of the current set of values mapped * from {@code key}. * * @param key * @return the set of mapped values for {@code key} */ protected Set<V> get(K key) { read.lock(); try { Set<V> values = present.get(key); return values != null ? values : emptyValues; } finally { read.unlock(); } } /** * Lazily retrieve the historical set of values for {@code key} at * {@code timestamp}. * * @param key * @param timestamp * @return the set of mapped values for {@code key} at {@code timestamp}. */ protected Set<V> get(K key, long timestamp) { read.lock(); try { Set<V> values = emptyValues; List<CompactRevision<V>> stored = history.get(key); if(stored != null) { values = Sets.newLinkedHashSet(); Iterator<CompactRevision<V>> it = stored.iterator(); while (it.hasNext()) { CompactRevision<V> revision = it.next(); if(revision.getVersion() <= timestamp) { if(revision.getType() == Action.ADD) { values.add(revision.getValue()); } else { values.remove(revision.getValue()); } } else { break; } } } return values; } finally { read.unlock(); } } /** * Initialize the appropriate data structure for the {@link #present}. * * @return the initialized mappings */ protected abstract Map<K, Set<V>> mapType(); /** * Return {@code true} if the action associated with {@code revision} * offsets the last action for an equal revision. * * @param revision * @return {@code true} if the revision if offset. */ private boolean isOffset(Revision<L, K, V> revision) { boolean contained = get(revision.getKey()) .contains(revision.getValue()); return ((revision.getType() == Action.ADD && !contained) || (revision .getType() == Action.REMOVE && contained)) ? true : false; } /** * An empty Set of type V that cannot be modified, but won't throw * exceptions. This returned in instances when a key does not map to any * values so that the caller can interact with the Set normally without * performing validity checks and while preserving data consistency. * * @author Jeff Nelson */ private final class EmptyValueSet implements Set<V> { @Override public boolean add(V e) { return false; } @Override public boolean addAll(Collection<? extends V> c) { return false; } @Override public void clear() {} @Override public boolean contains(Object o) { return false; } @Override public boolean containsAll(Collection<?> c) { return false; } @Override public boolean isEmpty() { return true; } @Override public Iterator<V> iterator() { return Collections.emptyIterator(); } @Override public boolean remove(Object o) { return false; } @Override public boolean removeAll(Collection<?> c) { return false; } @Override public boolean retainAll(Collection<?> c) { return false; } @Override public int size() { return 0; } @Override public Object[] toArray() { return null; } @Override public <T> T[] toArray(T[] a) { return null; } } }