/*
* 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.concurrent;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
import com.cinchapi.concourse.server.model.Text;
import com.cinchapi.concourse.server.model.Value;
import com.cinchapi.concourse.server.storage.Functions;
import com.cinchapi.concourse.thrift.Operator;
import com.cinchapi.concourse.thrift.TObject;
import com.cinchapi.concourse.util.Transformers;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.collect.Range;
import com.google.common.collect.RangeSet;
import com.google.common.collect.Sets;
import com.google.common.collect.TreeRangeSet;
/**
* A global service that provides ReadLock and WriteLock instances for a given
* {@link RangeToken}. The locks that are returned from this service can be used
* to lock <em>notions of things</em> that aren't strictly defined in their own
* right (i.e. a {@code key} in a {@code record})
* <p>
* </p>
* RangeLocks are designed to protect concurrent access to secondary indices
* (i.e. Reader A wants to find key between X and Z and Writer B wants to write
* Z), so they are governed by static logic that determine if, given an action,
* key, operator (optional) and value(s),it is acceptable to grab a lock
* representing the range.
* <p>
* <strong>WARNING</strong>: If the caller requests a lock for a given token,
* but does not attempt to grab it immediately, then it is possible that
* subsequent requests for locks identified by the same token will return
* different instances. This is unlikely to happen in practice, but it is
* recommended that lock grabs happen immediately after lock requests just to be
* safe (e.g
* <code>RangeLockService.getReadLock(key, operator, value).lock()</code>).
* </p>
*
* @author Jeff Nelson
*/
public class RangeLockService extends
AbstractLockService<RangeToken, RangeReadWriteLock> {
/**
* Create a new {@link RangeLockService}.
*
* @return the RangeLockService
*/
public static RangeLockService create() {
return new RangeLockService(
new ConcurrentHashMap<RangeToken, RangeReadWriteLock>());
}
/**
* Return a {@link RangeLockService} that does not actually provide any
* locks. This is used in situations where access is guaranteed (or at least
* assumed) to be isolated (e.g. a Transaction) and we need to simulate
* locking for polymorphic consistency.
*
* @return the LockService
*/
public static RangeLockService noOp() {
return NOOP_INSTANCE;
}
/**
* A {@link RangeLockService} that does not actually provide any locks. This
* is used in situations where access is guaranteed (or at least assumed) to
* be isolated (e.g. a Transaction) and we need to simulate locking for
* polymorphic consistency.
*/
private static final RangeLockService NOOP_INSTANCE = new RangeLockService() {
@Override
public ReadLock getReadLock(RangeToken token) {
return Locks.noOpReadLock();
}
@Override
public WriteLock getWriteLock(RangeToken token) {
return Locks.noOpWriteLock();
}
};
/**
* The information used in the {@link #isRangeBlocked(LockType, RangeToken)}
* method.
*/
protected final RangeBlockingInfo info = new RangeBlockingInfo();
private RangeLockService() {/* noop */}
/**
* Construct a new instance.
*
* @param locks
*/
private RangeLockService(ConcurrentMap<RangeToken, RangeReadWriteLock> locks) {
super(locks);
}
/**
* Return the ReadLock that is identified by {@code objects}. Every caller
* requesting a lock for {@code token} is guaranteed to get the same
* instance if the lock is currently held by a reader of a writer.
*
* @param objects
* @return the ReadLock
*/
public ReadLock getReadLock(String key, Operator operator,
TObject... values) {
return getReadLock(Text.wrapCached(key), operator,
Transformers.transformArray(values, Functions.TOBJECT_TO_VALUE,
Value.class));
}
/**
* Return the ReadLock that is identified by {@code objects}. Every caller
* requesting a lock for {@code token} is guaranteed to get the same
* instance if the lock is currently held by a reader of a writer.
*
* @param key
* @param operator
* @param values
* @return the ReadLock
*/
public ReadLock getReadLock(Text key, Operator operator, Value... values) {
return getReadLock(RangeToken.forReading(key, operator, values));
}
/**
* Return the WriteLock that is identified by {@code objects}. Every caller
* requesting a lock for {@code token} is guaranteed to get the same
* instance if the lock is currently held by a reader of a writer.
*
* @param key
* @param value
* @return the WriteLock
*/
public WriteLock getWriteLock(String key, TObject value) {
return getWriteLock(Text.wrapCached(key), Value.wrap(value));
}
/**
* Return the WriteLock that is identified by {@code objects}. Every caller
* requesting a lock for {@code token} is guaranteed to get the same
* instance if the lock is currently held by a reader of a writer.
*
* @param key
* @param value
* @return the WriteLock
*/
public WriteLock getWriteLock(Text key, Value value) {
return getWriteLock(RangeToken.forWriting(key, value));
}
@Override
protected RangeReadWriteLock createLock(RangeToken token) {
return new RangeReadWriteLock(this, token);
}
/**
* Return {@code true} if an attempt to used {@code token} for a
* {@code type} lock is range blocked. Range blocking occurs when there is
* another READ or WRITE happening such that allowing the proposed operation
* to proceed could lead to inconsistent results (i.e. I want to write X but
* there is a READ trying to find all values less than Y).
*
* @param type
* @param token
* @return {@code true} if range blocked
*/
protected final boolean isRangeBlocked(LockType type, RangeToken token) {
Value value = token.getValues()[0];
if(type == LockType.READ) {
Preconditions.checkArgument(token.getOperator() != null);
switch (token.getOperator()) {
case EQUALS:
return info.writes(token.getKey()).contains(value);
case NOT_EQUALS:
return info.writes(token.getKey()).size() > 1
|| (info.writes(token.getKey()).size() == 1 && !info
.writes(token.getKey()).contains(value));
default:
Iterator<Value> it = info.writes(token.getKey()).iterator();
while (it.hasNext()) {
Iterable<Range<Value>> ranges = RangeTokens
.convertToRange(token);
Value current = it.next();
Range<Value> point = Range.singleton(current);
for (Range<Value> range : ranges) {
RangeReadWriteLock lock = null;
if(range.isConnected(point)
&& !range.intersection(point).isEmpty()
&& (lock = locks.get(RangeToken.forWriting(
token.getKey(), current))) != null
&& !lock.isWriteLockedByCurrentThread()) {
return true;
}
}
}
return false;
}
}
else {
// If I want to WRITE X, I am blocked if there is a READ that
// touches X (e.g. direct read for X or a range read that includes
// X)
return info.reads(token.getKey()).contains(value);
}
}
/**
* A class that holds information that is used to determine if a thread is
* {@link RangeLockService#isRangeBlocked(LockType, RangeToken) range
* blocked} for a given lock acquisition attempt. This state of this
* information is updated externally in {@link RangeReadWriteLock} whenever
* locks are acquired and released.
*
* @author Jeff Nelson
*/
protected class RangeBlockingInfo {
/**
* Info about range read locks.
*/
private final ConcurrentMap<Text, RangeSet<Value>> reads = new ConcurrentHashMap<Text, RangeSet<Value>>();
/**
* Info about range write locks.
*/
private final ConcurrentMap<Text, Set<Value>> writes = new ConcurrentHashMap<Text, Set<Value>>();
/**
* Add a RANGE_READ for {@code key} that covers all of the
* {@code ranges}.
*
* @param key
* @param ranges
*/
public void add(Text key, Iterable<Range<Value>> ranges) {
RangeSet<Value> existing = reads.get(key);
if(existing == null) {
RangeSet<Value> created = TreeRangeSet.create();
existing = reads.putIfAbsent(key, created);
existing = MoreObjects.firstNonNull(existing, created);
}
synchronized (existing) {
for (Range<Value> range : ranges) {
existing.add(range);
}
}
}
/**
* Add a RANGE_WRITE for {@code key} that covers the {@code value}.
*
* @param key
* @param value
*/
public void add(Text key, Value value) {
Set<Value> existing = writes.get(key);
if(existing == null) {
Set<Value> created = Sets.newConcurrentHashSet();
existing = writes.putIfAbsent(key, created);
existing = MoreObjects.firstNonNull(existing, created);
}
existing.add(value);
}
/**
* Return all the ranges that are RANGE_READ locked for {@code key}.
*
* @param key
* @return the locked reads
*/
public RangeSet<Value> reads(Text key) {
RangeSet<Value> existing = reads.get(key);
if(existing == null) {
RangeSet<Value> created = TreeRangeSet.create();
existing = reads.putIfAbsent(key, created);
existing = MoreObjects.firstNonNull(existing, created);
}
synchronized (existing) {
return existing;
}
}
/**
* Remove the RANGE_READ for {@code key} that covers the {@code ranges}.
*
* @param key
* @param ranges
*/
public void remove(Text key, Iterable<Range<Value>> ranges) {
RangeSet<Value> existing = reads.get(key);
synchronized (existing) {
for (Range<Value> range : ranges) {
existing.remove(range);
}
}
}
/**
* Remove the RANGE_WRITE for {@code key} that covers the {@code value}.
*
* @param key
* @param value
*/
public void remove(Text key, Value value) {
Set<Value> existing = writes.get(key);
existing.remove(value);
}
/**
* Return all the values that are RANGE_WRITE locked for {@code key}.
*
* @param key
* @return the locked writes
*/
public Set<Value> writes(Text key) {
Set<Value> existing = writes.get(key);
if(existing == null) {
Set<Value> created = Sets.newConcurrentHashSet();
existing = writes.putIfAbsent(key, created);
existing = MoreObjects.firstNonNull(existing, created);
}
return existing;
}
}
}