Merge pull request #621 from nla/remove-hbase

Remove HBase from contrib
This commit is contained in:
Alex Osborne
2024-10-25 15:01:27 +09:00
committed by GitHub
14 changed files with 0 additions and 1838 deletions
-35
View File
@@ -24,41 +24,6 @@
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>0.98.6-cdh5.3.5</version>
<exclusions>
<exclusion>
<artifactId>jets3t</artifactId>
<groupId>net.java.dev.jets3t</groupId>
</exclusion>
<exclusion>
<artifactId>junit</artifactId>
<groupId>junit</groupId>
</exclusion>
<!-- tools.jar is not available in JDK 11 so exclude it
hbase-client accidentally leaked it as a transitive dependency
https://issues.apache.org/jira/browse/HBASE-13963 -->
<exclusion>
<groupId>jdk.tools</groupId>
<artifactId>jdk.tools</artifactId>
</exclusion>
<!-- prefer guava 17 from webarchive-commons over guava 12 from hbase-client -->
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
<exclusion>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</exclusion>
<exclusion>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
@@ -18,10 +18,6 @@
*/
package org.archive.modules.recrawl;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@@ -82,35 +78,7 @@ public class FetchHistoryHelper {
return null;
}
protected static final DateFormat HTTP_DATE_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz");
protected FetchHistoryHelper() {
}
/**
* converts time in HTTP Date format {@code dateStr} to seconds
* since epoch.
* @param dateStr time in HTTP Date format.
* @return seconds since epoch
*/
public static long parseHttpDate(String dateStr) {
synchronized (HTTP_DATE_FORMAT) {
try {
Date d = HTTP_DATE_FORMAT.parse(dateStr);
return d.getTime() / 1000;
} catch (ParseException ex) {
if (logger.isDebugEnabled())
logger.debug("bad HTTP DATE: " + dateStr);
return 0;
}
}
}
public static String formatHttpDate(long time) {
synchronized (HTTP_DATE_FORMAT) {
// format(Date) is not thread safe either
return HTTP_DATE_FORMAT.format(new Date(time * 1000));
}
}
}
@@ -1,126 +0,0 @@
/*
* This file is part of the Heritrix web crawler (crawler.archive.org).
*
* Licensed to the Internet Archive (IA) by one or more individual
* contributors.
*
* The IA 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.archive.modules.recrawl.hbase;
import java.io.IOException;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Logger;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.client.HBaseAdmin;
import org.springframework.context.Lifecycle;
/**
* Represents a deployment of HBase. (An instance, a database, an HBase...)
*
* @author nlevitt
*/
public class HBase implements Lifecycle {
private static final Logger logger =
Logger.getLogger(HBase.class.getName());
protected Configuration conf = null;
private Map<String,String> properties;
public Map<String,String> getProperties() {
return properties;
}
public void setProperties(Map<String,String> properties) {
this.properties = properties;
if (conf == null) {
conf = HBaseConfiguration.create();
}
for (Entry<String, String> entry: getProperties().entrySet()) {
conf.set(entry.getKey(), entry.getValue());
}
}
public synchronized Configuration configuration() {
if (conf == null) {
conf = HBaseConfiguration.create();
}
return conf;
}
// convenience setters
public void setZookeeperQuorum(String value) {
configuration().set("hbase.zookeeper.quorum", value);
}
public void setZookeeperClientPort(int port) {
configuration().setInt("hbase.zookeeper.property.clientPort", port);
}
protected transient HBaseAdmin admin;
public synchronized HBaseAdmin admin() throws IOException {
if (admin == null) {
admin = new HBaseAdmin(configuration());
}
return admin;
}
@Override
public synchronized void stop() {
isRunning = false;
if (admin != null) {
try {
admin.close();
} catch (IOException e) {
logger.warning("problem closing HBaseAdmin " + admin + " - " + e);
}
admin = null;
}
if (conf != null) {
// HConnectionManager.deleteConnection(conf); // XXX?
conf = null;
}
}
protected transient boolean isRunning = false;
@Override
public boolean isRunning() {
return isRunning;
}
@Override
public void start() {
isRunning = true;
}
public synchronized void reset() {
if (admin != null) {
try {
admin.close();
} catch (IOException e) {
logger.warning("problem closing HBaseAdmin " + admin + " - " + e);
}
admin = null;
}
}
}
@@ -1,303 +0,0 @@
package org.archive.modules.recrawl.hbase;
import static org.archive.modules.recrawl.RecrawlAttributeConstants.A_CONTENT_DIGEST_COUNT;
import static org.archive.modules.recrawl.RecrawlAttributeConstants.A_ORIGINAL_DATE;
import static org.archive.modules.recrawl.RecrawlAttributeConstants.A_ORIGINAL_URL;
import static org.archive.modules.recrawl.RecrawlAttributeConstants.A_WARC_FILENAME;
import static org.archive.modules.recrawl.RecrawlAttributeConstants.A_WARC_FILE_OFFSET;
import static org.archive.modules.recrawl.RecrawlAttributeConstants.A_WARC_RECORD_ID;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.hadoop.hbase.HColumnDescriptor;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.HBaseAdmin;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.RetriesExhaustedWithDetailsException;
import org.apache.hadoop.hbase.regionserver.NoSuchColumnFamilyException;
import org.apache.hadoop.hbase.util.Bytes;
import org.archive.modules.CrawlURI;
import org.archive.modules.recrawl.AbstractContentDigestHistory;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.context.Lifecycle;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
/**
* HBase content digest history store. Must be a toplevel bean in
* crawler-beans.cxml in order to receive {@link Lifecycle} events.
*
* @see AbstractContentDigestHistory
* @author nlevitt
*/
public class HBaseContentDigestHistory extends AbstractContentDigestHistory implements Lifecycle {
private static final Logger logger =
Logger.getLogger(HBaseContentDigestHistory.class.getName());
protected static final byte[] COLUMN_FAMILY = Bytes.toBytes("f");
protected static final byte[] COLUMN = Bytes.toBytes("c");
protected static final BiMap<String,String> JSON_KEYS_MAP = HashBiMap.create();
static {
JSON_KEYS_MAP.put(A_CONTENT_DIGEST_COUNT, "c");
JSON_KEYS_MAP.put(A_ORIGINAL_URL, "u");
JSON_KEYS_MAP.put(A_WARC_RECORD_ID, "i");
JSON_KEYS_MAP.put(A_WARC_FILENAME, "f");
JSON_KEYS_MAP.put(A_WARC_FILE_OFFSET, "o");
JSON_KEYS_MAP.put(A_ORIGINAL_DATE, "d");
}
protected HBaseTable table;
public void setTable(HBaseTable table) {
this.table = table;
}
protected boolean addColumnFamily = false;
public boolean getAddColumnFamily() {
return addColumnFamily;
}
/**
* Add the expected column family
* {@link #COLUMN_FAMILY} to the HBase table if the
* table doesn't already have it.
*/
public void setAddColumnFamily(boolean addColumnFamily) {
this.addColumnFamily = addColumnFamily;
}
protected int retryIntervalMs = 10*1000;
public int getRetryIntervalMs() {
return retryIntervalMs;
}
public void setRetryIntervalMs(int retryIntervalMs) {
this.retryIntervalMs = retryIntervalMs;
}
protected int maxTries = 1;
public int getMaxTries() {
return maxTries;
}
public void setMaxTries(int maxTries) {
this.maxTries = maxTries;
}
protected String keySuffix = null;
public String getKeySuffix() {
return keySuffix;
}
/**
* If not null, keySuffix is appended to the lookup key when loading and
* storing digest history. Thus the key looks like {digest}{keySuffix}, e.g.
* "sha1:22SFHXERHNFOEY6WK7YOUN4PFIPZSB4D-1193". The purpose is to support
* multiple namespaces in a single hbase table, to avoid proliferation of
* small tables. The reason we use a suffix instead of a prefix is to leave
* open the possibility of deduplication across these different namespaces
* at some point in the future.
*
* @param keySuffix
*/
public void setKeySuffix(String keySuffix) {
this.keySuffix = keySuffix;
}
@Override
protected String persistKeyFor(CrawlURI curi) {
if (keySuffix != null) {
return super.persistKeyFor(curi) + keySuffix;
} else {
return super.persistKeyFor(curi);
}
}
protected synchronized void addColumnFamily() {
try {
HTableDescriptor oldDesc = table.getHtableDescriptor();
if (oldDesc.getFamily(COLUMN_FAMILY) == null) {
HTableDescriptor newDesc = new HTableDescriptor(oldDesc);
newDesc.addFamily(new HColumnDescriptor(COLUMN_FAMILY));
logger.info("table does not yet have expected column family, modifying descriptor to " + newDesc);
HBaseAdmin hbaseAdmin = table.getHbase().admin();
hbaseAdmin.disableTable(table.getName());
hbaseAdmin.modifyTable(Bytes.toBytes(table.getName()), newDesc);
hbaseAdmin.enableTable(table.getName());
}
} catch (IOException e) {
logger.warning("problem adding column family: " + e);
}
}
private boolean isRunning;
@Override
public void start() {
// add column family here to avoid disabling table while another
// ToeThread is trying to use it
if (getAddColumnFamily()) {
addColumnFamily();
}
this.isRunning = true;
}
@Override
public void stop() {
this.isRunning = false;
}
@Override
public boolean isRunning() {
return isRunning;
}
@Override
public void load(CrawlURI curi) {
// make this call in all cases so that the value is initialized and
// WARCWriterProcessor knows it should put the info in there
HashMap<String, Object> contentDigestHistory = curi.getContentDigestHistory();
byte[] key = Bytes.toBytes(persistKeyFor(curi));
Result hbaseResult = tryHbaseGet(curi, new Get(key));
if (hbaseResult != null) {
Map<String, Object> loadedHistory = parseHbaseResult(curi, hbaseResult);
if (loadedHistory != null) {
if (logger.isLoggable(Level.FINER)) {
logger.finer("loaded history by digest " + persistKeyFor(curi)
+ " for uri " + curi + " - " + loadedHistory);
}
contentDigestHistory.putAll(loadedHistory);
}
}
}
protected Result tryHbaseGet(CrawlURI curi, Get hbaseGet) {
try {
return table.get(hbaseGet);
} catch (IOException e) {
logger.warning("problem retrieving persist data from hbase, proceeding without, for digest " + persistKeyFor(curi) + " uri " + curi + " - " + e);
return null;
}
}
protected Map<String, Object> parseHbaseResult(CrawlURI curi, Result hbaseResult) {
HashMap<String, Object> loadedHistory = null;
// no data for uri is indicated by empty Result
if (!hbaseResult.isEmpty()) {
byte[] jsonBytes = hbaseResult.getValue(COLUMN_FAMILY, COLUMN);
if (jsonBytes != null) {
JSONObject json = null;
try {
json = new JSONObject(Bytes.toString(jsonBytes));
loadedHistory = new HashMap<String,Object>();
@SuppressWarnings("unchecked")
Iterator<String> keyIter = json.keys();
while (keyIter.hasNext()) {
String jsonKey = keyIter.next();
Object jsonValue = json.get(jsonKey);
String historyMapKey = JSON_KEYS_MAP.inverse().get(jsonKey);
if (historyMapKey == null) {
logger.warning("unknown key \"" + jsonKey + "\" found in hbase json for digest " + persistKeyFor(curi));
historyMapKey = jsonKey;
}
loadedHistory.put(historyMapKey, jsonValue);
}
} catch (JSONException e) {
logger.warning("problem parsing json for digest " + persistKeyFor(curi) + " uri " + curi + " - " + e);
}
} else {
// shouldn't happen? result.isEmpty() is normal case
logger.fine("[jsonBytes==null] no persist data for digest " + persistKeyFor(curi) + " uri " + curi);
}
} else {
logger.finest("[result.isEmpty()] no persist data for digest " + persistKeyFor(curi) + " uri " + curi);
}
return loadedHistory;
}
@Override
public void store(CrawlURI curi) {
if (!curi.hasContentDigestHistory()
|| curi.getContentDigestHistory().isEmpty()) {
return;
}
if (logger.isLoggable(Level.FINER)) {
logger.finer("storing history by digest " + persistKeyFor(curi)
+ " for uri " + curi + " - "
+ curi.getContentDigestHistory());
}
Put hbasePut = createHbasePut(curi);
tryHbasePut(curi, hbasePut);
}
protected void tryHbasePut(CrawlURI curi, Put p) {
int tryCount = 0;
do {
tryCount++;
try {
table.put(p);
return;
} catch (RetriesExhaustedWithDetailsException e) {
if (e.getCause(0) instanceof NoSuchColumnFamilyException && getAddColumnFamily()) {
addColumnFamily();
tryCount--;
} else {
logger.warning("put failed " + "(try " + tryCount + " of "
+ getMaxTries() + ")" + " for " + curi + " - " + e);
}
} catch (IOException e) {
logger.warning("put failed " + "(try " + tryCount + " of "
+ getMaxTries() + ")" + " for " + curi + " - " + e);
} catch (NullPointerException e) {
// HTable.put() throws NullPointerException while connection is lost.
logger.warning("put failed " + "(try " + tryCount + " of "
+ getMaxTries() + ")" + " for " + curi + " - " + e);
}
if (tryCount > 0 && tryCount < getMaxTries() && isRunning()) {
try {
Thread.sleep(getRetryIntervalMs());
} catch (InterruptedException ex) {
logger.warning("thread interrupted. aborting retry for " + curi);
return;
}
}
} while (tryCount < getMaxTries() && isRunning());
if (isRunning()) {
logger.warning("giving up after " + tryCount + " tries on put for " + curi);
}
}
protected Put createHbasePut(CrawlURI curi) {
byte[] key = Bytes.toBytes(persistKeyFor(curi));
Put hbasePut = new Put(key);
try {
JSONObject json = new JSONObject();
for (Entry<String, Object> entry: curi.getContentDigestHistory().entrySet()) {
String jsonKey = JSON_KEYS_MAP.get(entry.getKey());
if (jsonKey == null) {
logger.warning("unknown key \"" + entry.getKey() + "\" found in content digest history map for " + curi);
jsonKey = entry.getKey();
}
json.put(jsonKey, entry.getValue());
}
hbasePut.add(COLUMN_FAMILY, COLUMN, Bytes.toBytes(json.toString()));
} catch (JSONException e) {
// should not happen - all values are either primitive or String.
logger.log(Level.SEVERE, "problem creating json object for digest " + persistKeyFor(curi) + " uri " + curi, e);
}
return hbasePut;
}
}
@@ -1,88 +0,0 @@
/*
* This file is part of the Heritrix web crawler (crawler.archive.org).
*
* Licensed to the Internet Archive (IA) by one or more individual
* contributors.
*
* The IA 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.archive.modules.recrawl.hbase;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.Result;
import org.archive.modules.CrawlURI;
import org.archive.modules.ProcessResult;
import org.archive.modules.Processor;
import org.archive.modules.recrawl.FetchHistoryProcessor;
/**
* A {@link Processor} for retrieving recrawl info from HBase table.
* See {@link HBasePersistProcessor} for table schema.
* As with other fetch history processors, this needs to be combined with {@link FetchHistoryProcessor}
* (set up after FetchHTTP, before WarcWriter) to work.
* @see HBasePersistStoreProcessor
* @author kenji
*/
public class HBasePersistLoadProcessor extends HBasePersistProcessor {
private static final Logger logger =
Logger.getLogger(HBasePersistLoadProcessor.class.getName());
@Override
protected ProcessResult innerProcessResult(CrawlURI uri) throws InterruptedException {
byte[] key = rowKeyForURI(uri);
Get g = new Get(key);
try {
Result r = table.get(g);
// no data for uri is indicated by empty Result
if (r.isEmpty()) {
if (logger.isLoggable(Level.FINE)) {
logger.fine(uri + ": <no crawlinfo>");
}
return ProcessResult.PROCEED;
}
schema.load(r, uri);
if (uri.getFetchStatus() < 0) {
return ProcessResult.FINISH;
}
} catch (IOException e) {
logger.warning("problem retrieving persist data from hbase, proceeding without, for " + uri + " - " + e);
} catch (Exception ex) {
// get() throws RuntimeException upon ZooKeeper connection failures.
// no crawl history load failure should make fetch of URL fail.
logger.log(Level.WARNING, "Get failed for " + uri + ": ", ex);
}
return ProcessResult.PROCEED;
}
/**
* unused.
*/
@Override
protected void innerProcess(CrawlURI uri) throws InterruptedException {
}
@Override
protected boolean shouldProcess(CrawlURI uri) {
// TODO: we want deduplicate robots.txt, too.
//if (uri.isPrerequisite()) return false;
String scheme = uri.getUURI().getScheme();
if (!(scheme.equals("http") || scheme.equals("https") || scheme.equals("ftp") || scheme.equals("sftp"))) {
return false;
}
return true;
}
}
@@ -1,32 +0,0 @@
package org.archive.modules.recrawl.hbase;
import org.archive.modules.CrawlURI;
import org.archive.modules.recrawl.AbstractPersistProcessor;
import org.springframework.beans.factory.annotation.Required;
/**
* A base class for processors for keeping de-duplication data in HBase.
* Table schema is defined by {@link RecrawlDataSchema} implementation.
* @author kenji
*/
public abstract class HBasePersistProcessor extends AbstractPersistProcessor {
protected HBaseTableBean table;
@Required
public void setTable(HBaseTableBean table) {
this.table = table;
}
protected RecrawlDataSchema schema;
public RecrawlDataSchema getSchema() {
return schema;
}
@Required
public void setSchema(RecrawlDataSchema schema) {
this.schema = schema;
}
protected byte[] rowKeyForURI(CrawlURI curi) {
return schema.rowKeyForURI(curi);
}
}
@@ -1,133 +0,0 @@
/*
* This file is part of the Heritrix web crawler (crawler.archive.org).
*
* Licensed to the Internet Archive (IA) by one or more individual
* contributors.
*
* The IA 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.archive.modules.recrawl.hbase;
import java.io.IOException;
import java.util.logging.Logger;
import org.apache.hadoop.hbase.HColumnDescriptor;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.client.HBaseAdmin;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.RetriesExhaustedWithDetailsException;
import org.apache.hadoop.hbase.regionserver.NoSuchColumnFamilyException;
import org.apache.hadoop.hbase.util.Bytes;
import org.archive.modules.CrawlURI;
import org.archive.modules.fetcher.FetchStatusCodes;
import org.archive.modules.recrawl.RecrawlAttributeConstants;
/**
* @author kenji
*/
public class HBasePersistStoreProcessor extends HBasePersistProcessor implements FetchStatusCodes, RecrawlAttributeConstants {
private static final Logger logger = Logger.getLogger(HBasePersistStoreProcessor.class.getName());
protected boolean addColumnFamily = false;
public boolean getAddColumnFamily() {
return addColumnFamily;
}
/**
* Add the expected column family
* {@link HBaseContentDigestHistory#COLUMN_FAMILY} to the HBase table if the
* table doesn't already have it.
*/
public void setAddColumnFamily(boolean addColumnFamily) {
this.addColumnFamily = addColumnFamily;
}
protected int retryIntervalMs = 10*1000;
public int getRetryIntervalMs() {
return retryIntervalMs;
}
public void setRetryIntervalMs(int retryIntervalMs) {
this.retryIntervalMs = retryIntervalMs;
}
protected int maxTries = 1;
public int getMaxTries() {
return maxTries;
}
public void setMaxTries(int maxTries) {
this.maxTries = maxTries;
}
protected synchronized void addColumnFamily() {
try {
HTableDescriptor oldDesc = table.getHtableDescriptor();
byte[] columnFamily = Bytes.toBytes(schema.getColumnFamily());
if (oldDesc.getFamily(columnFamily) == null) {
HTableDescriptor newDesc = new HTableDescriptor(oldDesc);
newDesc.addFamily(new HColumnDescriptor(columnFamily));
logger.info("table does not yet have expected column family, modifying descriptor to " + newDesc);
HBaseAdmin hbaseAdmin = table.getHbase().admin();
hbaseAdmin.disableTable(table.getName());
hbaseAdmin.modifyTable(Bytes.toBytes(table.getName()), newDesc);
hbaseAdmin.enableTable(table.getName());
}
} catch (IOException e) {
logger.warning("problem adding column family: " + e);
}
}
@Override
protected void innerProcess(CrawlURI uri) {
Put p = schema.createPut(uri);
int tryCount = 0;
do {
tryCount++;
try {
table.put(p);
return;
} catch (RetriesExhaustedWithDetailsException e) {
if (e.getCause(0) instanceof NoSuchColumnFamilyException && getAddColumnFamily()) {
addColumnFamily();
tryCount--;
} else {
logger.warning("put failed " + "(try " + tryCount + " of "
+ getMaxTries() + ")" + " for " + uri + " - " + e);
}
} catch (IOException e) {
logger.warning("put failed " + "(try " + tryCount + " of "
+ getMaxTries() + ")" + " for " + uri + " - " + e);
} catch (NullPointerException e) {
// HTable.put() throws NullPointerException while connection is lost.
logger.warning("put failed " + "(try " + tryCount + " of "
+ getMaxTries() + ")" + " for " + uri + " - " + e);
}
if (tryCount > 0 && tryCount < getMaxTries() && isRunning()) {
try {
Thread.sleep(getRetryIntervalMs());
} catch (InterruptedException ex) {
logger.warning("thread interrupted. aborting retry for " + uri);
return;
}
}
} while (tryCount < getMaxTries() && isRunning());
if (isRunning()) {
logger.warning("giving up after " + tryCount + " tries on put for " + uri);
}
}
@Override
protected boolean shouldProcess(CrawlURI curi) {
return super.shouldStore(curi);
}
}
@@ -1,159 +0,0 @@
/*
* This file is part of the Heritrix web crawler (crawler.archive.org).
*
* Licensed to the Internet Archive (IA) by one or more individual
* contributors.
*
* The IA 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.archive.modules.recrawl.hbase;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.HBaseAdmin;
import org.apache.hadoop.hbase.client.HConnection;
import org.apache.hadoop.hbase.client.HConnectionManager;
import org.apache.hadoop.hbase.client.HTableInterface;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Result;
/**
* @author kenji
* @author nlevitt
*/
public class HBaseTable extends HBaseTableBean {
static final Logger logger =
Logger.getLogger(HBaseTable.class.getName());
protected boolean create = false;
protected HConnection hconn = null;
protected ThreadLocal<HTableInterface> htable = new ThreadLocal<HTableInterface>();
public boolean getCreate() {
return create;
}
/** Create the named table if it doesn't exist. */
public void setCreate(boolean create) {
this.create = create;
}
public HBaseTable() {
}
protected synchronized HConnection hconnection() throws IOException {
if (hconn == null) {
hconn = HConnectionManager.createConnection(hbase.configuration());
}
return hconn;
}
protected HTableInterface htable() throws IOException {
if (htable.get() == null) {
htable.set(hconnection().getTable(htableName));
}
return htable.get();
}
@Override
public void put(Put p) throws IOException {
try {
htable().put(p);
} catch (IOException e) {
reset();
throw e;
}
}
@Override
public Result get(Get g) throws IOException {
try {
return htable().get(g);
} catch (IOException e) {
reset();
throw e;
}
}
public HTableDescriptor getHtableDescriptor() throws IOException {
try {
return htable().getTableDescriptor();
} catch (IOException e) {
reset();
throw e;
}
}
@Override
public void start() {
if (getCreate()) {
int attempt = 1;
while (true) {
try {
HBaseAdmin admin = hbase.admin();
if (!admin.tableExists(htableName)) {
HTableDescriptor desc = new HTableDescriptor(TableName.valueOf(htableName));
logger.info("hbase table '" + htableName + "' does not exist, creating it... " + desc);
admin.createTable(desc);
}
break;
} catch (IOException e) {
logger.log(Level.WARNING, "(attempt " + attempt + ") problem creating hbase table " + htableName, e);
attempt++;
reset();
// back off up to 60 seconds between retries
try {
Thread.sleep(Math.min(attempt * 1000, 60000));
} catch (InterruptedException e1) {
}
}
}
}
super.start();
}
protected void reset() {
if (htable.get() != null) {
try {
htable.get().close();
} catch (IOException e) {
logger.log(Level.WARNING, "htablename='" + htableName + "' htable.close() threw " + e, e);
}
htable.remove();
}
if (hconn != null) {
try {
hconn.close();
} catch (IOException e) {
logger.log(Level.WARNING, "hconn.close() threw " + e, e);
}
// HConnectionManager.deleteStaleConnection(hconn);
hconn = null;
}
hbase.reset();
}
@Override
public synchronized void stop() {
super.stop();
reset();
}
}
@@ -1,78 +0,0 @@
package org.archive.modules.recrawl.hbase;
import java.io.IOException;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Result;
import org.archive.modules.recrawl.PersistOnlineProcessor;
import org.springframework.context.Lifecycle;
/**
* base class for different types of HBaseTable Spring bean implementations.
* @author kenji
* @author nlevitt
*
*/
public abstract class HBaseTableBean implements Lifecycle {
protected String htableName = PersistOnlineProcessor.URI_HISTORY_DBNAME;
protected HBase hbase = new HBase();
protected transient boolean isRunning = false;
// <backward-compatibility>
public void setName(String name) {
this.htableName = name;
}
public String getName() {
return htableName;
}
// </backward-compatibility>
/**
* set name of single HTable this instance accesses.
* @param htableName
*/
public void setHtableName(String htableName) {
this.htableName = htableName;
}
public String getHtableName() {
return htableName;
}
public HBaseTableBean() {
super();
}
public void setHbase(HBase hbase) {
this.hbase = hbase;
}
public HBase getHbase() {
return hbase;
}
public abstract void put(Put p) throws IOException;
public abstract Result get(Get g) throws IOException;
public abstract HTableDescriptor getHtableDescriptor() throws IOException;
@Override
public boolean isRunning() {
return isRunning;
}
@Override
public void start() {
isRunning = true;
}
@Override
public synchronized void stop() {
isRunning = false;
}
}
@@ -1,140 +0,0 @@
/*
* This file is part of the Heritrix web crawler (crawler.archive.org).
*
* Licensed to the Internet Archive (IA) by one or more individual
* contributors.
*
* The IA 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.archive.modules.recrawl.hbase;
import java.util.Map;
import java.util.logging.Logger;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.util.Bytes;
import org.archive.modules.CrawlURI;
import org.archive.modules.fetcher.FetchStatusCodes;
import org.archive.modules.recrawl.FetchHistoryHelper;
import org.archive.modules.recrawl.RecrawlAttributeConstants;
/**
* RecrawlDataSchema that stores each recrawl data properties in a separate column in single column
* family, whose name may be configured with {@link #setColumnFamily(String)} (default "f").
* <ul>
* <li>{@code s}: fetch status (as integer text)</li>
* <li>{@code d}: content digest (with {@code sha1:} prefix, Base32 text)</li>
* <li>{@code e}: ETag (enclosing quotes stripped)</li>
* <li>{@code m}: last-modified date-time (as integer timestamp, binary format)</li>
* <li>{@code z}: do-not-crawl flag - loader discards URL if this column has non-empty value.</li>
* </ul>
*
* @author kenji
*/
public class MultiColumnRecrawlDataSchema extends RecrawlDataSchemaBase implements RecrawlDataSchema, RecrawlAttributeConstants {
static final Logger logger = Logger.getLogger(MultiColumnRecrawlDataSchema.class.getName());
public static final byte[] COLUMN_STATUS = Bytes.toBytes("s");
public static final byte[] COLUMN_CONTENT_DIGEST = Bytes.toBytes("d");
public static final byte[] COLUMN_ETAG = Bytes.toBytes("e");
public static final byte[] COLUMN_LAST_MODIFIED = Bytes.toBytes("m");
/* (non-Javadoc)
* @see org.archive.modules.hq.recrawl.RecrawlDataSchema#createPut()
*/
public Put createPut(CrawlURI uri) {
byte[] uriBytes = rowKeyForURI(uri);
byte[] key = uriBytes;
Put p = new Put(key);
String digest = uri.getContentDigestSchemeString();
if (digest != null) {
p.add(columnFamily, COLUMN_CONTENT_DIGEST, Bytes.toBytes(digest));
}
p.add(columnFamily, COLUMN_STATUS, Bytes.toBytes(Integer.toString(uri.getFetchStatus())));
if (uri.isHttpTransaction()) {
String etag = uri.getHttpResponseHeader(RecrawlAttributeConstants.A_ETAG_HEADER);
if (etag != null) {
// Etqg is usually quoted
if (etag.length() >= 2 && etag.charAt(0) == '"' && etag.charAt(etag.length() - 1) == '"')
etag = etag.substring(1, etag.length() - 1);
p.add(columnFamily, COLUMN_ETAG, Bytes.toBytes(etag));
}
String lastmod = uri.getHttpResponseHeader(RecrawlAttributeConstants.A_LAST_MODIFIED_HEADER);
if (lastmod != null) {
long lastmod_sec = FetchHistoryHelper.parseHttpDate(lastmod);
if (lastmod_sec == 0) {
try {
lastmod_sec = uri.getFetchCompletedTime();
} catch (NullPointerException ex) {
logger.warning("CrawlURI.getFetchCompletedTime():" + ex + " for " + uri.shortReportLine());
}
}
if (lastmod_sec != 0)
p.add(columnFamily, COLUMN_LAST_MODIFIED, Bytes.toBytes(lastmod_sec));
} else {
try {
long completed = uri.getFetchCompletedTime();
if (completed != 0)
p.add(columnFamily, COLUMN_LAST_MODIFIED, Bytes.toBytes(completed));
} catch (NullPointerException ex) {
logger.warning("CrawlURI.getFetchCompletedTime():" + ex + " for " + uri.shortReportLine());
}
}
}
return p;
}
/* (non-Javadoc)
* @see org.archive.modules.hq.recrawl.RecrawlDataSchema#load(java.util.Map, org.apache.hadoop.hbase.client.Result)
*/
public void load(Result result, CrawlURI curi) {
// check for "do-not-crawl" flag - any non-empty data tells not to crawl this
// URL.
byte[] nocrawl = result.getValue(columnFamily, COLUMN_NOCRAWL);
if (nocrawl != null && nocrawl.length > 0) {
// fetch status set to S_DEEMED_CHAFF, because this do-not-crawl flag
// is primarily intended for preventing crawler from stepping on traps.
curi.setFetchStatus(FetchStatusCodes.S_DEEMED_CHAFF);
curi.getAnnotations().add("nocrawl");
return;
}
// all column should have identical timestamp.
KeyValue rkv = result.getColumnLatest(columnFamily, COLUMN_STATUS);
long timestamp = rkv.getTimestamp();
Map<String, Object> history = FetchHistoryHelper.getFetchHistory(curi, timestamp, historyLength);
// FetchHTTP ignores history with status <= 0
byte[] status = result.getValue(columnFamily, COLUMN_STATUS);
if (status != null) {
// Note that status is stored as integer text. It's typically three-chars
// that is less than 4-byte integer bits.
history.put(RecrawlAttributeConstants.A_STATUS, Integer.parseInt(Bytes.toString(status)));
byte[] etag = result.getValue(columnFamily, COLUMN_ETAG);
if (etag != null) {
history.put(RecrawlAttributeConstants.A_ETAG_HEADER, Bytes.toString(etag));
}
byte[] lastmod = result.getValue(columnFamily, COLUMN_LAST_MODIFIED);
if (lastmod != null) {
long lastmod_sec = Bytes.toLong(lastmod);
history.put(RecrawlAttributeConstants.A_LAST_MODIFIED_HEADER, FetchHistoryHelper.formatHttpDate(lastmod_sec));
}
byte[] digest = result.getValue(columnFamily, COLUMN_CONTENT_DIGEST);
if (digest != null) {
history.put(RecrawlAttributeConstants.A_CONTENT_DIGEST, Bytes.toString(digest));
}
}
}
}
@@ -1,35 +0,0 @@
/*
* This file is part of the Heritrix web crawler (crawler.archive.org).
*
* Licensed to the Internet Archive (IA) by one or more individual
* contributors.
*
* The IA 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.archive.modules.recrawl.hbase;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Result;
import org.archive.modules.CrawlURI;
/**
* @author kenji
*/
public interface RecrawlDataSchema {
public String getColumnFamily();
public Put createPut(CrawlURI uri);
public void load(Result result, CrawlURI curi);
// TODO: drop this method by revising createPut(CrawlURI) method.
public byte[] rowKeyForURI(CrawlURI curi);
}
@@ -1,123 +0,0 @@
package org.archive.modules.recrawl.hbase;
import java.util.Map;
import java.util.logging.Logger;
import org.apache.hadoop.hbase.util.Bytes;
import org.archive.modules.CrawlURI;
import org.archive.modules.canonicalize.CanonicalizationRule;
import org.archive.modules.recrawl.FetchHistoryHelper;
import org.archive.modules.recrawl.FetchHistoryProcessor;
import org.archive.modules.recrawl.PersistProcessor;
/**
* implements common utility methods for implementing {@link RecrawlDataSchema}.
* <ul>
* <li>configuring single column family name</li>
* <li>formatting/parsing HTTP date text</li>
* <li>constructing row key</li>
* <li>preparing fetch-history array</li>
* </ul>
* @author kenji
*/
abstract public class RecrawlDataSchemaBase implements RecrawlDataSchema {
private static final Logger logger = Logger.getLogger(RecrawlDataSchemaBase.class.getName());
/**
* default value for {@link #columnFamily}.
*/
public static final byte[] DEFAULT_COLUMN_FAMILY = Bytes.toBytes("f");
protected byte[] columnFamily = DEFAULT_COLUMN_FAMILY;
public static final byte[] COLUMN_NOCRAWL = Bytes.toBytes("z");
/**
* default value for {@link #useCanonicalString}.
*/
public static boolean DEFAULT_USE_CANONICAL_STRING = true;
private boolean useCanonicalString = DEFAULT_USE_CANONICAL_STRING;
private CanonicalizationRule keyRule = null;
protected int historyLength = 2;
public RecrawlDataSchemaBase() {
super();
}
public void setColumnFamily(String colf) {
columnFamily = Bytes.toBytes(colf);
}
public boolean isUseCanonicalString() {
return useCanonicalString;
}
/**
* if set to true, canonicalized string will be used as row key, rather than URI
* @param useCanonicalString
*/
public void setUseCanonicalString(boolean useCanonicalString) {
this.useCanonicalString = useCanonicalString;
}
public String getColumnFamily() {
return Bytes.toString(columnFamily);
}
public CanonicalizationRule getKeyRule() {
return keyRule;
}
/**
* alternative canonicalization rule for generating row key from URI.
* TODO: currently unused.
* @param keyRule
*/
public void setKeyRule(CanonicalizationRule keyRule) {
this.keyRule = keyRule;
}
public int getHistoryLength() {
return historyLength;
}
/**
* maximum number of crawl history entries to retain in {@link CrawlURI}.
* when more than this number of crawl history entry is being added by
* {@link #getFetchHistory(CrawlURI, long)}, oldest entry will be discarded.
* {@code historyLength} should be the same number as
* {@link FetchHistoryProcessor#setHistoryLength(int)}, or FetchHistoryProcessor will
* reallocate the crawl history array.
* @param historyLength
* @see FetchHistoryProcessor#setHistoryLength(int)
*/
public void setHistoryLength(int historyLength) {
this.historyLength = historyLength;
}
/**
* calls {@link FetchHistoryHelper#getFetchHistory(CrawlURI, long, int)} with {@link #historyLength}.
* @param uri CrawlURI from which fetch history is obtained.
* @return Map object for storing re-crawl data (never null).
* @see FetchHistoryHelper#getFetchHistory(CrawlURI, long, int)
* @see FetchHistoryProcessor
*/
protected Map<String, Object> getFetchHistory(CrawlURI uri, long timestamp) {
return FetchHistoryHelper.getFetchHistory(uri, timestamp, historyLength);
}
/**
* return row key for {@code curi}.
* TODO: move this to HBasePersistProcessor by redesigning {@link RecrawlDataSchema}.
* @param curi {@link CrawlURI} for which a row is being fetched.
* @return row key
*/
public byte[] rowKeyForURI(CrawlURI curi) {
if (useCanonicalString) {
// TODO: use keyRule if specified.
return Bytes.toBytes(PersistProcessor.persistKeyFor(curi));
} else {
return Bytes.toBytes(curi.toString());
}
}
}
@@ -1,174 +0,0 @@
/*
* This file is part of the Heritrix web crawler (crawler.archive.org).
*
* Licensed to the Internet Archive (IA) by one or more individual
* contributors.
*
* The IA 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.archive.modules.recrawl.hbase;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.util.Bytes;
import org.archive.modules.CrawlURI;
import org.archive.modules.fetcher.FetchStatusCodes;
import org.archive.modules.recrawl.FetchHistoryHelper;
import org.archive.modules.recrawl.RecrawlAttributeConstants;
import org.json.JSONException;
import org.json.JSONObject;
/**
* {@linkplain SingleColumnJsonRecrawlDataSchema} stores all re-crawl data properties in a single column,
* in JSON format. As HBase stores each column paired with the row key, it takes a lot of space to store
* each re-crawl data property in its own column.
* <ul>
* <li>{@code r}: re-crawl data in JSON format</li>
* <li>{@code z}: do-not-crawl flag - loader discards URL if this column has non-empty value.</li>
* </ul>
* @author Kenji Nagahashi
*/
public class SingleColumnJsonRecrawlDataSchema extends RecrawlDataSchemaBase
implements RecrawlDataSchema {
static final Logger logger = Logger.getLogger(SingleColumnJsonRecrawlDataSchema.class.getName());
public static byte[] DEFAULT_COLUMN = Bytes.toBytes("r");
// JSON property names for re-crawl data properties
public static final String PROPERTY_STATUS = "s";
public static final String PROPERTY_CONTENT_DIGEST = "d";
public static final String PROPERTY_ETAG = "e";
public static final String PROPERTY_LAST_MODIFIED = "m";
// SHA1 scheme is assumed.
public static final String CONTENT_DIGEST_SCHEME = "sha1:";
// single column for storing JSON of re-crawl data
protected byte[] column = DEFAULT_COLUMN;
public void setColumn(String column) {
this.column = Bytes.toBytes(column);
}
public String getColumn() {
return Bytes.toString(column);
}
/* (non-Javadoc)
* @see org.archive.modules.hq.recrawl.RecrawlDataSchema#createPut(org.archive.modules.CrawlURI)
*/
public Put createPut(CrawlURI uri) {
byte[] key = rowKeyForURI(uri);
Put p = new Put(key);
JSONObject jo = new JSONObject();
try {
// TODO should we post warning message when scheme != "sha1"?
String digest = uri.getContentDigestString();
if (digest != null) {
jo.put(PROPERTY_CONTENT_DIGEST, digest);
}
jo.put(PROPERTY_STATUS, uri.getFetchStatus());
if (uri.isHttpTransaction()) {
String etag = uri.getHttpResponseHeader(RecrawlAttributeConstants.A_ETAG_HEADER);
if (etag != null) {
// Etag is usually quoted
if (etag.length() >= 2 && etag.charAt(0) == '"' && etag.charAt(etag.length() - 1) == '"')
etag = etag.substring(1, etag.length() - 1);
jo.put(PROPERTY_ETAG, etag);
}
String lastmod = uri.getHttpResponseHeader(RecrawlAttributeConstants.A_LAST_MODIFIED_HEADER);
if (lastmod != null) {
long lastmod_sec = FetchHistoryHelper.parseHttpDate(lastmod);
if (lastmod_sec == 0) {
try {
lastmod_sec = uri.getFetchCompletedTime();
} catch (NullPointerException ex) {
logger.warning("CrawlURI.getFetchCompletedTime():" + ex + " for " + uri.shortReportLine());
}
}
} else {
try {
long completed = uri.getFetchCompletedTime();
if (completed != 0)
jo.put(PROPERTY_LAST_MODIFIED, completed);
} catch (NullPointerException ex) {
logger.warning("CrawlURI.getFetchCompletedTime():" + ex + " for " + uri.shortReportLine());
}
}
}
} catch (JSONException ex) {
// should not happen - all values are either primitive or String.
logger.log(Level.SEVERE, "JSON translation failed", ex);
}
p.add(columnFamily, column, Bytes.toBytes(jo.toString()));
return p;
}
/* (non-Javadoc)
* @see org.archive.modules.hq.recrawl.RecrawlDataSchema#load(org.apache.hadoop.hbase.client.Result)
*/
public void load(Result result, CrawlURI curi) {
// check for "do-not-crawl" flag - any non-empty data tells not to crawl this
// URL.
byte[] nocrawl = result.getValue(columnFamily, COLUMN_NOCRAWL);
if (nocrawl != null && nocrawl.length > 0) {
// fetch status set to S_DEEMED_CHAFF, because this do-not-crawl flag
// is primarily intended for preventing crawler from stepping on traps.
curi.setFetchStatus(FetchStatusCodes.S_DEEMED_CHAFF);
curi.getAnnotations().add("nocrawl");
return;
}
KeyValue rkv = result.getColumnLatest(columnFamily, column);
long timestamp = rkv.getTimestamp();
Map<String, Object> history = FetchHistoryHelper.getFetchHistory(curi, timestamp, historyLength);
if (history == null) {
// crawl history array is fully occupied by crawl history entries
// newer than timestamp.
return;
}
byte[] jsonBytes = rkv.getValue();
if (jsonBytes != null) {
JSONObject jo = null;
try {
jo = new JSONObject(Bytes.toString(jsonBytes));
} catch (JSONException ex) {
logger.warning(String.format("JSON parsing failed for key %1s: %2s",
result.getRow(), ex.getMessage()));
}
if (jo != null) {
int status = jo.optInt(PROPERTY_STATUS, -1);
if (status >= 0) {
history.put(RecrawlAttributeConstants.A_STATUS, status);
}
String digest = jo.optString(PROPERTY_CONTENT_DIGEST);
if (digest != null) {
history.put(RecrawlAttributeConstants.A_CONTENT_DIGEST, CONTENT_DIGEST_SCHEME + digest);
}
String etag = jo.optString(PROPERTY_ETAG);
if (etag != null) {
history.put(RecrawlAttributeConstants.A_ETAG_HEADER, etag);
}
long lastmod = jo.optLong(PROPERTY_LAST_MODIFIED);
if (lastmod > 0) {
history.put(RecrawlAttributeConstants.A_LAST_MODIFIED_HEADER, FetchHistoryHelper.formatHttpDate(lastmod));
}
}
}
}
}
@@ -1,380 +0,0 @@
/*
* This file is part of the Heritrix web crawler (crawler.archive.org).
*
* Licensed to the Internet Archive (IA) by one or more individual
* contributors.
*
* The IA 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.archive.modules.recrawl.hbase;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.NotServingRegionException;
import org.apache.hadoop.hbase.TableNotFoundException;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.hbase.client.HTableInterface;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.util.Bytes;
/**
* simple HTable wrapper that shares single instance of HTable among threads.
* If you only perform get on HTable, this implementation
* should be good enough. If multiple threads performs Put, {@link HBaseTable}
* would be more efficient.
* <p>when HBase I/O fails due to issue with network/region server/zookeeper, this
* class waits for preset time (see {@link #setReconnectInterval(int)})
* before trying to reestablish HBase connection. During this hold-ff period, all
* {@link #get(Get)} and {@link #put(Put)} calls will fail.
*
* @author kenji
*/
public class SingleHBaseTable extends HBaseTableBean {
private static final Log LOG = LogFactory.getLog(SingleHBaseTable.class);
private HTableInterface table;
private volatile long tableError;
private ReentrantReadWriteLock tableUseLock = new ReentrantReadWriteLock();
boolean autoReconnect = true;
public boolean isAutoReconnect() {
return autoReconnect;
}
/**
* if set to {@code true}, HBaseClient tries to reconnect to the HBase master
* immediately when Put request failed due to connection loss (note {@link #put(Put)}
* still throws IOException even if autoReconnect is enabled.)
* @param autoReconnect true to enable auto-reconnect
*/
public void setAutoReconnect(boolean autoReconnect) {
this.autoReconnect = autoReconnect;
}
protected boolean autoFlush = true;
/**
* passed on to HTable's autoFlush property upon creation.
* @return true for enabling auto-flush.
*/
public boolean isAutoFlush() {
return autoFlush;
}
public void setAutoFlush(boolean autoFlush) {
this.autoFlush = autoFlush;
}
// default 3 minutes
private int reconnectInterval = 1000 * 3 * 60;
public int getReconnectInterval() {
return reconnectInterval;
}
/**
* set hold-off interval upon communication errors.
* @param reconnectInterval hold-off interval in milliseconds.
*/
public void setReconnectInterval(int reconnectInterval) {
this.reconnectInterval = reconnectInterval;
}
// counters
protected AtomicLong getCount = new AtomicLong();
// count of GET/PUT failures (i.e. not counting connection failures).
protected AtomicLong getErrorCount = new AtomicLong();
protected AtomicLong getSkipCount = new AtomicLong();
protected AtomicLong putCount = new AtomicLong();
protected AtomicLong putErrorCount = new AtomicLong();
protected AtomicLong putSkipCount = new AtomicLong();
protected AtomicLong connectCount = new AtomicLong();
public long getGetCount() { return getCount.get(); }
public long getGetErrorCount() { return getErrorCount.get(); }
public long getGetSkipCount() { return getSkipCount.get(); }
public long getPutCount() { return putCount.get(); }
public long getConnectCount() { return connectCount.get(); }
// for diagnosing deadlock situation
public Map<String, Object> getTableLockState() {
Map<String, Object> m = new LinkedHashMap<String, Object>();
m.put("readLockCount", tableUseLock.getReadLockCount());
m.put("queueLength", tableUseLock.getQueueLength());
m.put("writeLocked", tableUseLock.isWriteLocked());
return m;
}
public SingleHBaseTable() {
}
/**
* attempts to reconnect to HBase if table is null.
* must not be called with read-lock.
* @return existing or newly opened HTableInterface.
*/
protected HTableInterface getTable() {
if (table == null && autoReconnect)
openTable();
return table;
}
/**
* close HTable {@code table}, set current time to tableError if closing because
* of a communication error. should be called with write lock.
* @param htable HTable to close.
* @param byError true if closing because of an error.
*/
protected void closeTable(HTableInterface htable, boolean byError) {
if (htable == null) return;
if (table != htable) {
// other thread did closeTable on htable. don't close table.
return;
}
try {
table = null;
htable.close();
} catch (IOException ex) {
LOG.warn("error closing " + htable + " - some commits may have been lost");
}
if (byError) {
tableError = System.currentTimeMillis();
}
}
public void put(Put p) throws IOException {
putCount.incrementAndGet();
// trigger reconnection if necessary. as table can be modified before
// read lock is acquired, we don't read table variable here.
getTable();
boolean htableFailed = false;
HTableInterface htable = null;
Lock readLock = tableUseLock.readLock();
try {
if (!readLock.tryLock(TRY_READ_LOCK_TIMEOUT, TimeUnit.SECONDS)) {
putSkipCount.incrementAndGet();
throw new IOException("could not acquire read lock for HTable.");
}
} catch (InterruptedException ex) {
throw new IOException("interrupted while acquiring read lock", ex);
}
try {
htable = table;
if (htable == null) {
putSkipCount.incrementAndGet();
throw new IOException("HBase connection is unvailable.");
}
// HTable.put() buffers Puts and access to the buffer is not
// synchronized.
synchronized (htable) {
try {
htable.put(p);
} catch (NullPointerException ex) {
// HTable.put() throws NullPointerException when connection is lost.
// It is somewhat weird, so translate it to IOException.
putErrorCount.incrementAndGet();
htableFailed = true;
throw new IOException("hbase connection is lost", ex);
} catch (NotServingRegionException ex) {
putErrorCount.incrementAndGet();
// no need to close HTable.
throw ex;
} catch (IOException ex) {
putErrorCount.incrementAndGet();
htableFailed = true;
throw ex;
}
}
} finally {
readLock.unlock();
if (htableFailed) {
closeTable(htable, true);
}
}
}
public Result get(Get g) throws IOException {
getCount.incrementAndGet();
// trigger reconnection if necessary. as table can be modified before
// read lock is acquired, we don't read table variable here.
getTable();
boolean htableFailed = false;
HTableInterface htable = null;
Lock readLock = tableUseLock.readLock();
try {
if (!readLock.tryLock(TRY_READ_LOCK_TIMEOUT, TimeUnit.SECONDS)) {
getSkipCount.incrementAndGet();
throw new IOException("could not acquire read lock for HTable.");
}
} catch (InterruptedException ex) {
throw new IOException("interrupted while acquiring read lock", ex);
}
try {
htable = table;
if (htable == null) {
getSkipCount.incrementAndGet();
throw new IOException("HBase connection is unvailable.");
}
try {
return htable.get(g);
} catch (NotServingRegionException ex) {
// caused by disruption to HBase cluster. no need to
// refresh HBase connection, since connection itself
// is working okay.
// TODO: should we need to back-off for a while? other
// regions may still be accessible.
getErrorCount.incrementAndGet();
throw ex;
} catch (IOException ex) {
getErrorCount.incrementAndGet();
htableFailed = true;
throw ex;
}
} finally {
readLock.unlock();
if (htableFailed) {
closeTable(htable, true);
}
}
}
@Override
public HTableDescriptor getHtableDescriptor() throws IOException {
HTableInterface table = getTable();
if (table == null) {
throw new IOException("HBase connection is unavailable.");
}
return table.getTableDescriptor();
}
public boolean inBackoffPeriod() {
return (tableError > 0 &&
(System.currentTimeMillis() - tableError) < reconnectInterval);
}
/**
* timestamp of the last Put/Get error.
* @return timestamp in ms.
*/
public long getTableErrorTime() {
return tableError;
}
/**
* connect to HBase.
* it does nothing if table is non-null, or it is in the back-off period since
* the last error.
* should be called with write lock.
*/
protected boolean openTable() {
if (table != null) return true;
// fail immediately if we're in back-off period.
if (inBackoffPeriod()) return false;
try {
HTable t = new HTable(hbase.configuration(), Bytes.toBytes(htableName));
connectCount.incrementAndGet();
t.setAutoFlush(autoFlush);
table = t;
tableError = 0;
return true;
} catch (TableNotFoundException ex) {
// ex.getMessage() only has table name. be a little bit more friendly.
LOG.warn("failed to connect to HTable \"" + htableName + "\": Table Not Found");
tableError = System.currentTimeMillis();
return false;
} catch (IOException ex) {
LOG.warn("failed to connect to HTable \"" + htableName + "\" (" + ex.getMessage() + ")");
tableError = System.currentTimeMillis();
return false;
}
}
/**
* number of seconds to wait for acquiring read lock.
* if read lock is not acquired within this many seconds (probably
* due to deadlock situation on write-lock side), {@link #get(Get)} will
* <i>silently</i> fail.
*/
public final static long TRY_READ_LOCK_TIMEOUT = 5;
/**
* number of seconds to wait for acquiring write lock.
*/
public final static long TRY_WRITE_LOCK_TIMEOUT = 10;
/**
* close current connection and establish new connection.
* fails silently if back-off period is in effect.
*/
protected void reconnect(boolean onerror) throws IOException, InterruptedException {
// avoid deadlock situation caused by attempting
// to acquire write lock while holding read lock.
// there'd be no real dead-lock now that timeout on write lock is implemented,
// but it's nice to know there's a bug in locking.
if (tableUseLock.getReadHoldCount() > 0) {
LOG.warn("avoiding deadlock: reconnect() called by thread with read lock.");
return;
}
Lock writeLock = tableUseLock.writeLock();
if (!writeLock.tryLock(TRY_WRITE_LOCK_TIMEOUT, TimeUnit.SECONDS)) {
LOG.warn("reconnect() could not acquire write lock on tableUseLock for " +
TRY_WRITE_LOCK_TIMEOUT + "s, giving up.");
return;
}
try {
closeTable(table, onerror);
openTable();
} finally {
writeLock.unlock();
}
}
/**
* close current connection and establish new connection.
* for refreshing stale connection through scripting.
* resets tableErrorTime to zero (it will be set to non-zero if
* reconnection attempt fails).
* @throws IOException
* @throws InterruptedException
*/
public void reconnect() throws IOException, InterruptedException {
tableError = 0;
reconnect(false);
}
// public boolean isRunning() {
// return table != null;
// }
public void start() {
super.start();
openTable();
}
public void stop() {
if (table != null) {
try {
table.close();
} catch (IOException ex) {
LOG.warn("table.close() failed", ex);
}
}
table = null;
super.stop();
}
}