/*
* 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.hadoop.hbase.thrift;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.hbase.thrift.ThriftServerRunner.HBaseHandler;
import org.apache.hadoop.hbase.thrift.generated.TIncrement;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.metrics.util.MBeanUtil;
import org.apache.thrift.TException;
/**
* This class will coalesce increments from a thift server if
* hbase.regionserver.thrift.coalesceIncrement is set to true. Turning this
* config to true will cause the thrift server to queue increments into an
* instance of this class. The thread pool associated with this class will drain
* the coalesced increments as the thread is able. This can cause data loss if the
* thrift server dies or is shut down before everything in the queue is drained.
*
*/
public class IncrementCoalescer implements IncrementCoalescerMBean {
/**
* Used to identify a cell that will be incremented.
*
*/
static class FullyQualifiedRow {
private byte[] table;
private byte[] rowKey;
private byte[] family;
private byte[] qualifier;
public FullyQualifiedRow(byte[] table, byte[] rowKey, byte[] fam, byte[] qual) {
super();
this.table = table;
this.rowKey = rowKey;
this.family = fam;
this.qualifier = qual;
}
public byte[] getTable() {
return table;
}
public void setTable(byte[] table) {
this.table = table;
}
public byte[] getRowKey() {
return rowKey;
}
public void setRowKey(byte[] rowKey) {
this.rowKey = rowKey;
}
public byte[] getFamily() {
return family;
}
public void setFamily(byte[] fam) {
this.family = fam;
}
public byte[] getQualifier() {
return qualifier;
}
public void setQualifier(byte[] qual) {
this.qualifier = qual;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(family);
result = prime * result + Arrays.hashCode(qualifier);
result = prime * result + Arrays.hashCode(rowKey);
result = prime * result + Arrays.hashCode(table);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
FullyQualifiedRow other = (FullyQualifiedRow) obj;
if (!Arrays.equals(family, other.family)) return false;
if (!Arrays.equals(qualifier, other.qualifier)) return false;
if (!Arrays.equals(rowKey, other.rowKey)) return false;
if (!Arrays.equals(table, other.table)) return false;
return true;
}
}
static class DaemonThreadFactory implements ThreadFactory {
static final AtomicInteger poolNumber = new AtomicInteger(1);
final ThreadGroup group;
final AtomicInteger threadNumber = new AtomicInteger(1);
final String namePrefix;
DaemonThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
namePrefix = "ICV-" + poolNumber.getAndIncrement() + "-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
if (!t.isDaemon()) t.setDaemon(true);
if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
private final AtomicLong failedIncrements = new AtomicLong();
private final AtomicLong successfulCoalescings = new AtomicLong();
private final AtomicLong totalIncrements = new AtomicLong();
private final ConcurrentMap<FullyQualifiedRow, Long> countersMap =
new ConcurrentHashMap<FullyQualifiedRow, Long>(100000, 0.75f, 1500);
private final ThreadPoolExecutor pool;
private final HBaseHandler handler;
private int maxQueueSize = 500000;
private static final int CORE_POOL_SIZE = 1;
protected final Log LOG = LogFactory.getLog(this.getClass().getName());
@SuppressWarnings("deprecation")
public IncrementCoalescer(HBaseHandler hand) {
this.handler = hand;
LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>();
pool =
new ThreadPoolExecutor(CORE_POOL_SIZE, CORE_POOL_SIZE, 50, TimeUnit.MILLISECONDS, queue,
new DaemonThreadFactory());
MBeanUtil.registerMBean("thrift", "Thrift", this);
}
public boolean queueIncrement(TIncrement inc) throws TException {
if (!canQueue()) {
failedIncrements.incrementAndGet();
return false;
}
return internalQueueTincrement(inc);
}
public boolean queueIncrements(List<TIncrement> incs) throws TException {
if (!canQueue()) {
failedIncrements.incrementAndGet();
return false;
}
for (TIncrement tinc : incs) {
internalQueueTincrement(tinc);
}
return true;
}
private boolean internalQueueTincrement(TIncrement inc) throws TException {
byte[][] famAndQf = KeyValue.parseColumn(inc.getColumn());
if (famAndQf.length < 1) return false;
byte[] qual = famAndQf.length == 1 ? new byte[0] : famAndQf[1];
return internalQueueIncrement(inc.getTable(), inc.getRow(), famAndQf[0], qual,
inc.getAmmount());
}
private boolean internalQueueIncrement(byte[] tableName, byte[] rowKey, byte[] fam,
byte[] qual, long ammount) throws TException {
int countersMapSize = countersMap.size();
//Make sure that the number of threads is scaled.
dynamicallySetCoreSize(countersMapSize);
totalIncrements.incrementAndGet();
FullyQualifiedRow key = new FullyQualifiedRow(tableName, rowKey, fam, qual);
long currentAmount = ammount;
// Spin until able to insert the value back without collisions
while (true) {
Long value = countersMap.remove(key);
if (value == null) {
// There was nothing there, create a new value
value = Long.valueOf(currentAmount);
} else {
value += currentAmount;
successfulCoalescings.incrementAndGet();
}
// Try to put the value, only if there was none
Long oldValue = countersMap.putIfAbsent(key, value);
if (oldValue == null) {
// We were able to put it in, we're done
break;
}
// Someone else was able to put a value in, so let's remember our
// current value (plus what we picked up) and retry to add it in
currentAmount = value;
}
// We limit the size of the queue simply because all we need is a
// notification that something needs to be incremented. No need
// for millions of callables that mean the same thing.
if (pool.getQueue().size() <= 1000) {
// queue it up
Callable<Integer> callable = createIncCallable();
pool.submit(callable);
}
return true;
}
public boolean canQueue() {
return countersMap.size() < maxQueueSize;
}
private Callable<Integer> createIncCallable() {
return new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int failures = 0;
Set<FullyQualifiedRow> keys = countersMap.keySet();
for (FullyQualifiedRow row : keys) {
Long counter = countersMap.remove(row);
if (counter == null) {
continue;
}
try {
HTable table = handler.getTable(row.getTable());
if (failures > 2) {
throw new IOException("Auto-Fail rest of ICVs");
}
table.incrementColumnValue(row.getRowKey(), row.getFamily(), row.getQualifier(),
counter);
} catch (IOException e) {
// log failure of increment
failures++;
LOG.error("FAILED_ICV: " + Bytes.toString(row.getTable()) + ", "
+ Bytes.toStringBinary(row.getRowKey()) + ", "
+ Bytes.toStringBinary(row.getFamily()) + ", "
+ Bytes.toStringBinary(row.getQualifier()) + ", " + counter);
}
}
return failures;
}
};
}
/**
* This method samples the incoming requests and, if selected, will check if
* the corePoolSize should be changed.
* @param countersMapSize
*/
private void dynamicallySetCoreSize(int countersMapSize) {
// Here we are using countersMapSize as a random number, meaning this
// could be a Random object
if (countersMapSize % 10 != 0) {
return;
}
double currentRatio = (double) countersMapSize / (double) maxQueueSize;
int newValue = 1;
if (currentRatio < 0.1) {
// it's 1
} else if (currentRatio < 0.3) {
newValue = 2;
} else if (currentRatio < 0.5) {
newValue = 4;
} else if (currentRatio < 0.7) {
newValue = 8;
} else if (currentRatio < 0.9) {
newValue = 14;
} else {
newValue = 22;
}
if (pool.getCorePoolSize() != newValue) {
pool.setCorePoolSize(newValue);
}
}
// MBean get/set methods
public int getQueueSize() {
return pool.getQueue().size();
}
public int getMaxQueueSize() {
return this.maxQueueSize;
}
public void setMaxQueueSize(int newSize) {
this.maxQueueSize = newSize;
}
public long getPoolCompletedTaskCount() {
return pool.getCompletedTaskCount();
}
public long getPoolTaskCount() {
return pool.getTaskCount();
}
public int getPoolLargestPoolSize() {
return pool.getLargestPoolSize();
}
public int getCorePoolSize() {
return pool.getCorePoolSize();
}
public void setCorePoolSize(int newCoreSize) {
pool.setCorePoolSize(newCoreSize);
}
public int getMaxPoolSize() {
return pool.getMaximumPoolSize();
}
public void setMaxPoolSize(int newMaxSize) {
pool.setMaximumPoolSize(newMaxSize);
}
public long getFailedIncrements() {
return failedIncrements.get();
}
public long getSuccessfulCoalescings() {
return successfulCoalescings.get();
}
public long getTotalIncrements() {
return totalIncrements.get();
}
public long getCountersMapSize() {
return countersMap.size();
}
}