/*
* Copyright 2010 Bizosys Technologies Limited
*
* Licensed to the Bizosys Technologies Limited (Bizosys) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The Bizosys 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 com.bizosys.hsearch.hbase;
import java.io.IOException;
import java.util.List;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.client.Delete;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.RowLock;
import com.bizosys.hsearch.common.ByteField;
import com.bizosys.hsearch.filter.IStorable;
import com.bizosys.hsearch.util.ObjectFactory;
import com.bizosys.hsearch.util.Record;
import com.bizosys.hsearch.util.RecordScalar;
/**
* All HBase write calls goes from here.
* It supports Insert, Delete, Update and Merge operations.
* Merge is a operation, where read and write happens inside
* a lock. This lock is never exposed to caller function.
* @author karan
*
*/
public class HWriter {
private static final boolean DEBUG_ENABLED = HbaseLog.l.isDebugEnabled();
//private boolean isBatchMode = false;
private static HWriter singleton = null;
/**
* Factory for getting HWriter instance.
* Currently HWriter can execute in a thread safe environment with
* multiple writers originating from a singel machine or multi
* machine environment or out of a single thread write environment.
* @param enableThreadSafety Should it run in a parallel clients mode
* @return HWriter instance.
*/
public static HWriter getInstance(boolean enableThreadSafety ) {
if ( null != singleton) return singleton;
synchronized (HWriter.class) {
if ( null != singleton) return singleton;
singleton = new HWriter();
}
return singleton;
}
/**
* Default constructor.
* Don't use
*/
private HWriter() {
}
/**
* Insert just a single scalar record. If the record is already existing, it overrides.
* A scalar record contains just one column.
* @param tableName Table name
* @param record A Table record
* @throws IOException
*/
public void insertScalar(String tableName, RecordScalar record) throws IOException {
if (DEBUG_ENABLED) HbaseLog.l.debug("HWriter> insertScalar:record " + tableName);
byte[] pk = record.pk.toBytes();
Put update = new Put(pk);
NV kv = record.kv;
update.add(kv.family.toBytes(),kv.name.toBytes(), kv.data.toBytes());
update.setWriteToWAL(true);
HTableWrapper table = null;
HBaseFacade facade = null;
try {
facade = HBaseFacade.getInstance();
table = facade.getTable(tableName);
table.put(update);
table.flushCommits();
} finally {
if ( null != facade && null != table) {
facade.putTable(table);
}
}
}
/**
* Insert multiple scalar records. If records exist, it overrides
* A scalar record contains just one column.
* @param tableName Table name
* @param records Table records
* @throws IOException
*/
public void insertScalar(String tableName,
List<RecordScalar> records) throws IOException {
if (DEBUG_ENABLED) HbaseLog.l.debug("HWriter> insertScalar:records table " + tableName);
List<Put> updates = ObjectFactory.getInstance().getPutList();
for (RecordScalar record : records) {
Put update = new Put(record.pk.toBytes());
NV kv = record.kv;
update.add(kv.family.toBytes(),kv.name.toBytes(), kv.data.toBytes());
update.setWriteToWAL(true);
updates.add(update);
}
HTableWrapper table = null;
HBaseFacade facade = null;
try {
facade = HBaseFacade.getInstance();
table = facade.getTable(tableName);
table.put(updates);
table.flushCommits();
} finally {
if ( null != facade && null != table) {
facade.putTable(table);
}
if ( null != updates) ObjectFactory.getInstance().putPutsList(updates);
}
}
/**
* Insert a record
* @param tableName
* @param record
* @throws IOException
*/
public void insert(String tableName, Record record) throws IOException {
if (DEBUG_ENABLED) HbaseLog.l.debug("HWriter> insert to table " + tableName);
HTableWrapper table = null;
HBaseFacade facade = null;
try {
Put update = new Put(record.pk.toBytes());
for (NV param : record.getNVs()) {
update.add(param.family.toBytes(),
param.name.toBytes(), param.data.toBytes());
}
update.setWriteToWAL(true);
facade = HBaseFacade.getInstance();
table = facade.getTable(tableName);
table.put(update);
table.flushCommits();
} finally {
if ( null != facade && null != table) {
facade.putTable(table);
}
}
}
/**
* Inserting multiple records. It overrides the values of existing records.
* from the time we have read..
* @param tableName
* @param records
* @throws IOException
*/
public void insert(String tableName, List<Record> records) throws IOException {
if (DEBUG_ENABLED) HbaseLog.l.debug("HWriter> insert:records to table " + tableName);
List<Put> updates = ObjectFactory.getInstance().getPutList();
for (Record record : records) {
Put update = new Put(record.pk.toBytes());
for (NV param : record.getNVs()) {
update.add(param.family.toBytes(),
param.name.toBytes(), param.data.toBytes());
}
update.setWriteToWAL(true);
updates.add(update);
}
HTableWrapper table = null;
HBaseFacade facade = null;
try {
facade = HBaseFacade.getInstance();
table = facade.getTable(tableName);
if (DEBUG_ENABLED) HbaseLog.l.debug("HWriter> insert:Putting records " + updates.size());
table.put(updates);
table.flushCommits();
} finally {
if ( null != facade && null != table) {
facade.putTable(table);
}
if ( null != updates) ObjectFactory.getInstance().putPutsList(updates);
}
}
/**
* Update a table. It calls back the update call back function for
* various modifications during update operations as bytes merging.
* @param tableName
* @param pk
* @param pipe
* @param families
* @throws IOException
*/
public void update(String tableName,
byte[] pk, IUpdatePipe pipe, byte[][] families) throws IOException {
if ( null == tableName || null == pk) return;
if (DEBUG_ENABLED) HbaseLog.l.debug("HWriter> update to table " + tableName);
HTableWrapper table = null;
HBaseFacade facade = null;
RowLock lock = null;
try {
facade = HBaseFacade.getInstance();
table = facade.getTable(tableName);
/**
* Scope down the existance check getter, not to mingle with actual one.
*/
if ( 1 == 1) {
Get existanceGet = new Get(pk);
if ( ! table.exists(existanceGet) ) return;
}
lock = table.lockRow(pk);
Get lockedGet = ( null == lock) ? new Get(pk) : new Get(pk,lock);
if ( null != families) {
for (byte[] family : families) {
lockedGet = lockedGet.addFamily(family);
}
}
Put lockedUpdate = null;
Delete lockedDelete = null;
int familiesT = ( null == families) ? 0 : families.length;
int[] familyByteLen = new int[familiesT];
Result r = table.get(lockedGet);
if ( null == r) return;
if ( null == r.list()) return;
for (KeyValue kv : r.list()) {
byte[] curVal = kv.getValue();
if ( null == curVal) continue;
if ( 0 == curVal.length) continue;
byte[] modifiedB = pipe.process(kv.getFamily(), kv.getQualifier(), curVal);
int modifiedBLen = ( null == modifiedB) ? 0 : modifiedB.length;
/**
* Count if family to be chucked out
* */
for (int i=0; i<familiesT; i++) {
byte[] family = families[i];
if ( ByteField.compareBytes(kv.getFamily(), family)) {
familyByteLen[i] = familyByteLen[i] + modifiedBLen;
}
}
boolean changedValue = false;
if ( 0 == modifiedBLen) {
if ( null == lockedDelete ) lockedDelete =
( null == lock) ? new Delete(pk) : new Delete(pk, -1, lock);
lockedDelete = lockedDelete.deleteColumn(kv.getFamily(), kv.getQualifier());
continue;
}
/**
* If changed, perform an update
*/
if (curVal.length == modifiedBLen) {
changedValue = ! ByteField.compareBytes(curVal, modifiedB);
} else {
changedValue = true;
}
if ( changedValue) {
if ( null == lockedUpdate )
lockedUpdate = ( null == lock) ? new Put(pk) : new Put(pk,lock);
KeyValue updatedKV = new KeyValue(r.getRow(),
kv.getFamily(), kv.getQualifier(),modifiedB);
lockedUpdate.add(updatedKV);
}
}
/**
* Flush all updates.
*/
if ( null != lockedUpdate ) {
lockedUpdate.setWriteToWAL(true);
table.put(lockedUpdate);
}
/**
* Flush all deletes
*/
if ( null != lockedDelete ) {
for (int i=0; i<familiesT; i++) {
if ( familyByteLen[i] == 0 ) {
lockedDelete = lockedDelete.deleteFamily(families[i]);
}
}
table.delete(lockedDelete);
}
if ( null != lockedUpdate || null != lockedDelete) table.flushCommits();
} finally {
boolean goodTable = true;
try {
if ( null != lock ) table.unlockRow(lock);
} catch (Exception ex) {
reportUnlockException(ex);
goodTable = false;
}
if ( null != facade && null != table && goodTable) {
facade.putTable(table);
}
}
}
/**
* Delete the complete row based on the key
* @param tableName Table name
* @param pk Serialized primary Key
* @throws IOException
*/
public void delete(String tableName, IStorable pk) throws IOException {
if ( null == pk) return;
Delete delete = new Delete(pk.toBytes());
HBaseFacade facade = HBaseFacade.getInstance();
HTableWrapper table = null;
try {
table = facade.getTable(tableName);
table.delete(delete);
table.flushCommits();
} finally {
if ( null != facade && null != table) {
facade.putTable(table);
}
}
}
/**
* Deletes the supplied columns for the row.
* @param tableName Table name
* @param pk Storable Primary Key
* @param packet ColumnFamily and ColumnName necessary
* @throws IOException
*/
public void delete(String tableName, IStorable pk, NV packet) throws IOException {
Delete delete = new Delete(pk.toBytes());
delete = delete.deleteColumns(packet.family.toBytes(), packet.name.toBytes());
HBaseFacade facade = HBaseFacade.getInstance();
HTableWrapper table = null;
try {
table = facade.getTable(tableName);
table.delete(delete);
table.flushCommits();
} finally {
if ( null != facade && null != table) {
facade.putTable(table);
}
}
}
/**
* Before putting the record, it merges the record.
* @param tableName Table name
* @param records Records
* @throws IOException
*/
public void mergeScalar(String tableName, List<RecordScalar> records)
throws IOException {
if ( null == tableName || null == records) return;
if (DEBUG_ENABLED)
HbaseLog.l.debug("HWriter: mergeScalar (" + tableName + ") , Count =" + records.size());
HTableWrapper table = null;
HBaseFacade facade = null;
List<RowLock> locks = ObjectFactory.getInstance().getRowLockList();
List<Put> updates = ObjectFactory.getInstance().getPutList();
try {
facade = HBaseFacade.getInstance();
table = facade.getTable(tableName);
for (RecordScalar scalar : records) {
byte[] pk = scalar.pk.toBytes();
if ( 0 == pk.length) continue;;
Get getter = new Get(pk);
byte[] famB = scalar.kv.family.toBytes();
byte[] nameB = scalar.kv.name.toBytes();
RowLock lock = null;
if ( table.exists(getter) ) {
lock = table.lockRow(pk);
if ( null == lock) {
throw new IOException("Unable to aquire lock," + new String(pk) +
" for the table - " + tableName);
}
locks.add(lock);
Get existingGet = (null == lock) ? new Get(pk) : new Get(pk, lock);
existingGet = existingGet.addColumn(famB, nameB);
Result r = table.get(existingGet);
if ( ! scalar.merge(r.getValue(famB, nameB)) ) {
if ( null != lock ) {
table.unlockRow(lock);
locks.remove(lock);
}
continue;
}
}
NV kv = scalar.kv;
byte[] data = kv.data.toBytes();
if ( null == data ) {
try {
if ( null != lock ) {
table.unlockRow(lock);
updates.remove(lock);
lock = null;
}
} catch (Exception ex) {
HbaseLog.l.warn("HWriter:mergeScalar > Ignore Unlock exp :" , ex);
}
continue;
}
Put update = ( null == lock ) ? new Put(pk) : new Put(pk, lock);
update.add(famB,nameB, data);
update.setWriteToWAL(true);
updates.add(update);
}
table.put(updates);
table.flushCommits();
} finally {
boolean goodTable = true;
for (RowLock lock: locks) {
try {
if ( null != lock ) table.unlockRow(lock);
} catch (Exception ex) {
reportUnlockException(ex);
goodTable = false;
}
}
if ( null != facade && null != table && goodTable) {
facade.putTable(table);
}
if ( null != locks ) ObjectFactory.getInstance().putRowLockList(locks);
if ( null != updates ) ObjectFactory.getInstance().putPutsList(updates);
}
}
/**
* Merge a record accessing the existing value
* It happens with the locking mechanism
* @param tableName Table name
* @param record A record
* @throws IOException
*/
public void merge(String tableName, Record record)
throws IOException {
if ( null == tableName || null == record) return;
if (DEBUG_ENABLED)
HbaseLog.l.debug("HWriter:merge Record (" + tableName + ")") ;
HTableWrapper table = null;
HBaseFacade facade = null;
RowLock lock = null;
try {
byte[] pk = record.pk.toBytes();
facade = HBaseFacade.getInstance();
table = facade.getTable(tableName);
//Step 0 : If does exists no need to merge.. Just insert.
Get existsCheck = new Get(pk);
if ( ! table.exists(existsCheck) ) {
insert(tableName, record);
return;
}
//Step 1 : Aquire a lock before merging
if (DEBUG_ENABLED) HbaseLog.l.debug("HWriter> Locking Row " );
lock = table.lockRow(pk);
if ( null == lock) {
throw new IOException("HWriter:merge Unable to aquire lock," + new String(pk) +
" for the table - " + tableName);
}
Get existingGet = ( null == lock) ? new Get(pk) : new Get(pk, lock);
for (NV nv : record.getBlankNVs()) {
existingGet = existingGet.addColumn(nv.family.toBytes(), nv.name.toBytes());
}
//Step 2 : Merge data with existing values
Result r = table.get(existingGet);
if ( null != r) {
if ( null != r.list()) {
for (KeyValue kv : r.list()) {
byte[] existingB = kv.getValue();
if ( null == existingB) continue;
if ( 0 == existingB.length)continue;
record.merge(kv.getFamily(),kv.getQualifier(), existingB);
}
}
}
//Step 3 : Only add values which have changed.
Put update = ( null == lock ) ? new Put(pk) : new Put(pk, lock);
int totalCols = 0;
for (NV nv : record.getNVs()) {
byte[] data = nv.data.toBytes();
if ( nv.isDataUnchanged) continue;
if (DEBUG_ENABLED) HbaseLog.l.debug("HWriter> data Size " + data.length);
update = update.add(nv.family.toBytes(), nv.name.toBytes(), data);
totalCols++;
}
//Step 4 : If no change.. Nothing to do.
if ( totalCols == 0 ) return;
//Step 5 : Write the changes.
update.setWriteToWAL(true);
if (DEBUG_ENABLED) HbaseLog.l.debug("HWriter> Committing Updates" );
table.put(update);
table.flushCommits();
} finally {
boolean goodTable = true;
if ( null != lock ) {
if (DEBUG_ENABLED) HbaseLog.l.debug("HWriter> Un Locking Row " );
try { table.unlockRow(lock); } catch (Exception ex) {
reportUnlockException(ex);
goodTable = false;
}
}
if ( null != facade && null != table && goodTable) {
facade.putTable(table);
}
}
}
private void reportUnlockException(Exception ex) {
Runtime runTime = Runtime.getRuntime();
String errorMsg = "Max Mem: " + runTime.maxMemory()/1024;
errorMsg = errorMsg + ", Total Mem: " + runTime.totalMemory()/1024;
errorMsg = errorMsg + ", Free Mem: " + runTime.freeMemory()/1024;
HbaseLog.l.warn("HWriter:reportUnlockException > Ignoring Unlock exp. May be memory Issue \n" + errorMsg, ex);
}
}