/**
* Licensed to the Apache Software Foundation (ASF) 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 org.apache.hive.common.util;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.hadoop.hive.common.Pool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
/** Simple object pool of limited size. Implemented as a lock-free ring buffer;
* may fail to produce items if there are too many concurrent users. */
public class FixedSizedObjectPool<T> implements Pool<T> {
public static final Logger LOG = LoggerFactory.getLogger(FixedSizedObjectPool.class);
/**
* Ring buffer has two "markers" - where objects are present ('objects' list), and where they are
* removed ('empty' list). This class contains bit shifts and masks for one marker's components
* within a long, and provides utility methods to get/set the components.
* Marker consists of (examples here for 'objects' list; same for 'empty' list):
* - the marker itself. Set to NO_MARKER if list is empty (e.g. no objects to take from pool),
* otherwise contains the array index of the first element of the list.
* - the 'delta'. Number of elements from the marker that is being modified. Each concurrent
* modification (e.g. take call) increments this to claim an array index. Delta elements
* from the marker cannot be touched by other threads. Delta can never overshoot the other
* marker (or own marker if other is empty), or overflow MAX_DELTA. If delta is set to
* NO_DELTA, it means the marker has been modified during 'take' operation and list cannot
* be touched (see below). In any of these cases, take returns null.
* - the 'refcount'/'rc'. Number of operations occurring on the marker. Each e.g. take incs
* this; when the last of the overlapping operations decreases the refcount, it 'commits'
* the modifications by moving the marker according to delta and resetting delta to 0.
* If the other list does not exist, it's also created (i.e. first 'offer' to a new pool with
* empty 'objects' list will create the 'objects' list); if the list is being exhausted to empty
* by other op (e.g. pool has 2 objects, 2 takes are in progress when offer commits), the
* marker of the other list is still reset to new location, and delta is set to NO_DELTA,
* preventing operations on the lists until the exhausting ops commit and set delta to 0.
*/
private static final class Marker {
// Currently the long must fit 2 markers. Setting these bit sizes determines the balance
// between max pool size allowed and max concurrency allowed. This balance here is not what we
// want (up to 254 of each op while only 65535 objects limit), but it uses whole bytes and is
// good for now. Delta and RC take the same number of bits; usually it doesn't make sense to
// have more delta.
private static final long MARKER_MASK = 0xffffL, DELTA_MASK = 0xffL, RC_MASK = 0xffL;
public Marker(int markerShift, int deltaShift, int rcShift) {
this.markerShift = markerShift;
this.deltaShift = deltaShift;
this.rcShift = rcShift;
}
int markerShift, deltaShift, rcShift;
public final long setMarker(long dest, long val) {
return setValue(dest, val, markerShift, MARKER_MASK);
}
public final long setDelta(long dest, long val) {
return setValue(dest, val, deltaShift, DELTA_MASK);
}
public final long setRc(long dest, long val) {
return setValue(dest, val, rcShift, RC_MASK);
}
public final long getMarker(long src) {
return getValue(src, markerShift, MARKER_MASK);
}
public final long getDelta(long src) {
return getValue(src, deltaShift, DELTA_MASK);
}
public final long getRc(long src) {
return getValue(src, rcShift, RC_MASK);
}
private final long setValue(long dest, long val, int offset, long mask) {
return (dest & (~(mask << offset))) + (val << offset);
}
private final long getValue(long src, int offset, long mask) {
return (src >>> offset) & mask;
}
public String toString(long markers) {
return "{" + getMarker(markers) + ", " + getDelta(markers) + ", " + getRc(markers) + "}";
}
}
private static final long NO_MARKER = Marker.MARKER_MASK, NO_DELTA = Marker.DELTA_MASK,
MAX_DELTA = NO_DELTA - 1, MAX_SIZE = NO_MARKER - 1;
private static final long NO_INDEX = 0; // The array index can't be reserved.
// See Marker class comment.
private static final Marker OBJECTS = new Marker(48, 40, 32);
private static final Marker EMPTY = new Marker(16, 8, 0);
private final AtomicLong state;
private final PoolObjectHelper<T> helper;
private final T[] pool;
public FixedSizedObjectPool(int size, PoolObjectHelper<T> helper) {
this(size, helper, LOG.isTraceEnabled());
}
@VisibleForTesting
public FixedSizedObjectPool(int size, PoolObjectHelper<T> helper, boolean doTraceLog) {
if (size > MAX_SIZE) {
throw new AssertionError("Size must be <= " + MAX_SIZE);
}
this.helper = helper;
@SuppressWarnings("unchecked")
T[] poolTmp = (T[])new Object[size];
pool = poolTmp;
// Initially, all deltas and rcs are 0; empty list starts at 0; there are no objects to take.
state = new AtomicLong(OBJECTS.setMarker(0, NO_MARKER));
casLog = doTraceLog ? new CasLog() : null;
}
@Override
public T take() {
T result = pool.length > 0 ? takeImpl() : null;
return (result == null) ? helper.create() : result;
}
@Override
public void offer(T t) {
tryOffer(t);
}
@Override
public int size() {
return pool.length;
}
@VisibleForTesting
public boolean tryOffer(T t) {
if (t == null || pool.length == 0) return false; // 0 size means no-pooling case - passthru.
helper.resetBeforeOffer(t);
return offerImpl(t);
}
private T takeImpl() {
long oldState = reserveArrayIndex(OBJECTS, EMPTY);
if (oldState == NO_INDEX) return null; // For whatever reason, reserve failed.
long originalMarker = OBJECTS.getMarker(oldState), delta = OBJECTS.getDelta(oldState);
int arrayIndex = (int)getArrayIndex(originalMarker, delta);
T result = pool[arrayIndex];
if (result == null) {
throwError(oldState, arrayIndex, "null");
}
pool[arrayIndex] = null;
commitArrayIndex(OBJECTS, EMPTY, originalMarker);
return result;
}
private boolean offerImpl(T t) {
long oldState = reserveArrayIndex(EMPTY, OBJECTS);
if (oldState == NO_INDEX) return false; // For whatever reason, reserve failed.
long originalMarker = EMPTY.getMarker(oldState), delta = EMPTY.getDelta(oldState);
int arrayIndex = (int)getArrayIndex(originalMarker, delta);
if (pool[arrayIndex] != null) {
throwError(oldState, arrayIndex, "non-null");
}
pool[arrayIndex] = t;
commitArrayIndex(EMPTY, OBJECTS, originalMarker);
return true;
}
private void throwError(long oldState, int arrayIndex, String type) {
long newState = state.get();
if (casLog != null) {
casLog.dumpLog(true);
}
String msg = "Unexpected " + type + " at " + arrayIndex + "; state was "
+ toString(oldState) + ", now " + toString(newState);
LOG.info(msg);
throw new AssertionError(msg);
}
private long reserveArrayIndex(Marker from, Marker to) {
while (true) {
long oldVal = state.get(), marker = from.getMarker(oldVal), delta = from.getDelta(oldVal),
rc = from.getRc(oldVal), toMarker = to.getMarker(oldVal), toDelta = to.getDelta(oldVal);
if (marker == NO_MARKER) return NO_INDEX; // The list is empty.
if (delta == MAX_DELTA) return NO_INDEX; // Too many concurrent operations; spurious failure.
if (delta == NO_DELTA) return NO_INDEX; // List is drained and recreated concurrently.
if (toDelta == NO_DELTA) { // Same for the OTHER list; spurious.
// TODO: the fact that concurrent re-creation of other list necessitates full stop is not
// ideal... the reason is that the list NOT being re-created still uses the list
// being re-created for boundary check; it needs the old value of the other marker.
// However, NO_DELTA means the other marker was already set to a new value. For now,
// assume concurrent re-creation is rare and the gap before commit is tiny.
return NO_INDEX;
}
assert rc <= delta; // There can never be more concurrent takers than uncommitted ones.
long newDelta = incDeltaValue(marker, toMarker, delta); // Increase target list pos.
if (newDelta == NO_DELTA) return NO_INDEX; // Target list is being drained.
long newVal = from.setRc(from.setDelta(oldVal, newDelta), rc + 1); // Set delta and refcount.
if (setState(oldVal, newVal)) return oldVal;
}
}
private void commitArrayIndex(Marker from, Marker to, long originalMarker) {
while (true) {
long oldVal = state.get(), rc = from.getRc(oldVal);
long newVal = from.setRc(oldVal, rc - 1); // Decrease refcount.
assert rc > 0;
if (rc == 1) {
// We are the last of the concurrent operations to finish. Commit.
long marker = from.getMarker(oldVal), delta = from.getDelta(oldVal),
otherMarker = to.getMarker(oldVal), otherDelta = to.getDelta(oldVal);
assert rc <= delta;
// Move marker according to delta, change delta to 0.
long newMarker = applyDeltaToMarker(marker, otherMarker, delta);
newVal = from.setDelta(from.setMarker(newVal, newMarker), 0);
if (otherMarker == NO_MARKER) {
// The other list doesn't exist, create it at the first index of our op.
assert otherDelta == 0;
newVal = to.setMarker(newVal, originalMarker);
} else if (otherDelta > 0 && otherDelta != NO_DELTA
&& applyDeltaToMarker(otherMarker, marker, otherDelta) == NO_MARKER) {
// The other list will be exhausted when it commits. Create new one pending that commit.
newVal = to.setDelta(to.setMarker(newVal, originalMarker), NO_DELTA);
}
}
if (setState(oldVal, newVal)) return;
}
}
private boolean setState(long oldVal, long newVal) {
boolean result = state.compareAndSet(oldVal, newVal);
if (result && casLog != null) {
casLog.log(oldVal, newVal);
}
return result;
}
private long incDeltaValue(long markerFrom, long otherMarker, long delta) {
if (delta == pool.length) return NO_DELTA; // The (pool-sized) list is being fully drained.
long result = delta + 1;
if (getArrayIndex(markerFrom, result) == getArrayIndex(otherMarker, 1)) {
return NO_DELTA; // The list is being drained, cannot increase the delta anymore.
}
return result;
}
private long applyDeltaToMarker(long marker, long markerLimit, long delta) {
if (delta == NO_DELTA) return marker; // List was recreated while we were exhausting it.
if (delta == pool.length) {
assert markerLimit == NO_MARKER; // If we had the entire pool, other list couldn't exist.
return NO_MARKER; // We exhausted the entire-pool-sized list.
}
marker = getArrayIndex(marker, delta); // Just move the marker according to delta.
if (marker == markerLimit) return NO_MARKER; // We hit the limit - the list was exhausted.
return marker;
}
private long getArrayIndex(long marker, long delta) {
marker += delta;
if (marker >= pool.length) {
marker -= pool.length; // Wrap around at the end of buffer.
}
return marker;
}
static String toString(long markers) {
return OBJECTS.toString(markers) + ", " + EMPTY.toString(markers);
}
// TODO: Temporary for debugging. Doesn't interfere with MTT failures (unlike LOG.debug).
private final static class CasLog {
private final int size;
private final long[] log;
private final AtomicLong offset = new AtomicLong(-1);
public CasLog() {
size = 1 << 14 /* 256Kb in longs */;
log = new long[size];
}
public void log(long oldVal, long newVal) {
int ix = (int)((offset.incrementAndGet() << 1) & (size - 1));
log[ix] = oldVal;
log[ix + 1] = newVal;
}
public synchronized void dumpLog(boolean doSleep) {
if (doSleep) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
int logSize = (int)offset.get();
// TODO: dump the end if wrapping around?
for (int i = 0; i < logSize; ++i) {
LOG.info("CAS history dump: " + FixedSizedObjectPool.toString(log[i << 1]) + " => "
+ FixedSizedObjectPool.toString(log[(i << 1) + 1]));
}
offset.set(0);
}
}
private final CasLog casLog;
}