From 5333d8f1b470dcfc04f46dffc9f467a176039764 Mon Sep 17 00:00:00 2001
From: Duo Zhang
Date: Mon, 15 Jul 2019 21:35:06 +0800
Subject: [PATCH] HBASE-22684 The log rolling request maybe canceled
immediately in LogRoller due to a race (#378)
Signed-off-by: Michael Stack
---
.../hadoop/hbase/regionserver/LogRoller.java | 173 ++++++++----------
.../hbase/regionserver/wal/AbstractFSWAL.java | 28 ++-
.../hbase/regionserver/wal/AsyncFSWAL.java | 2 +
.../hadoop/hbase/regionserver/wal/FSHLog.java | 2 +
.../hbase/regionserver/TestWALLockup.java | 2 +-
.../regionserver/wal/TestAsyncFSWAL.java | 132 ++++++++++++-
6 files changed, 228 insertions(+), 111 deletions(-)
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/LogRoller.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/LogRoller.java
index e72be5de072..3f057d0d0d5 100644
--- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/LogRoller.java
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/LogRoller.java
@@ -20,11 +20,13 @@ package org.apache.hadoop.hbase.regionserver;
import java.io.Closeable;
import java.io.IOException;
+import java.net.ConnectException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.locks.ReentrantLock;
-
+import java.util.concurrent.ConcurrentMap;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.Server;
import org.apache.hadoop.hbase.regionserver.wal.AbstractFSWAL;
@@ -37,6 +39,7 @@ import org.apache.hadoop.ipc.RemoteException;
import org.apache.yetus.audience.InterfaceAudience;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+
import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting;
/**
@@ -52,43 +55,47 @@ import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesti
@VisibleForTesting
public class LogRoller extends HasThread implements Closeable {
private static final Logger LOG = LoggerFactory.getLogger(LogRoller.class);
- private final ReentrantLock rollLock = new ReentrantLock();
- private final AtomicBoolean rollLog = new AtomicBoolean(false);
- private final ConcurrentHashMap walNeedsRoll = new ConcurrentHashMap<>();
+ private final ConcurrentMap walNeedsRoll = new ConcurrentHashMap<>();
private final Server server;
protected final RegionServerServices services;
- private volatile long lastrolltime = System.currentTimeMillis();
+ private volatile long lastRollTime = System.currentTimeMillis();
// Period to roll log.
- private final long rollperiod;
+ private final long rollPeriod;
private final int threadWakeFrequency;
// The interval to check low replication on hlog's pipeline
private long checkLowReplicationInterval;
private volatile boolean running = true;
- public void addWAL(final WAL wal) {
- if (null == walNeedsRoll.putIfAbsent(wal, Boolean.FALSE)) {
- wal.registerWALActionsListener(new WALActionsListener() {
- @Override
- public void logRollRequested(WALActionsListener.RollRequestReason reason) {
- walNeedsRoll.put(wal, Boolean.TRUE);
- // TODO logs will contend with each other here, replace with e.g. DelayedQueue
- synchronized(rollLog) {
- rollLog.set(true);
- rollLog.notifyAll();
+ public void addWAL(WAL wal) {
+ // check without lock first
+ if (walNeedsRoll.containsKey(wal)) {
+ return;
+ }
+ // this is to avoid race between addWAL and requestRollAll.
+ synchronized (this) {
+ if (walNeedsRoll.putIfAbsent(wal, Boolean.FALSE) == null) {
+ wal.registerWALActionsListener(new WALActionsListener() {
+ @Override
+ public void logRollRequested(WALActionsListener.RollRequestReason reason) {
+ // TODO logs will contend with each other here, replace with e.g. DelayedQueue
+ synchronized (LogRoller.this) {
+ walNeedsRoll.put(wal, Boolean.TRUE);
+ LogRoller.this.notifyAll();
+ }
}
- }
- });
+ });
+ }
}
}
public void requestRollAll() {
- for (WAL wal : walNeedsRoll.keySet()) {
- walNeedsRoll.put(wal, Boolean.TRUE);
- }
- synchronized(rollLog) {
- rollLog.set(true);
- rollLog.notifyAll();
+ synchronized (this) {
+ List wals = new ArrayList(walNeedsRoll.keySet());
+ for (WAL wal : wals) {
+ walNeedsRoll.put(wal, Boolean.TRUE);
+ }
+ notifyAll();
}
}
@@ -97,7 +104,7 @@ public class LogRoller extends HasThread implements Closeable {
super("LogRoller");
this.server = server;
this.services = services;
- this.rollperiod = this.server.getConfiguration().
+ this.rollPeriod = this.server.getConfiguration().
getLong("hbase.regionserver.logroll.period", 3600000);
this.threadWakeFrequency = this.server.getConfiguration().
getInt(HConstants.THREAD_WAKE_FREQUENCY, 10 * 1000);
@@ -105,19 +112,10 @@ public class LogRoller extends HasThread implements Closeable {
"hbase.regionserver.hlog.check.lowreplication.interval", 30 * 1000);
}
- @Override
- public void interrupt() {
- // Wake up if we are waiting on rollLog. For tests.
- synchronized (rollLog) {
- this.rollLog.notify();
- }
- super.interrupt();
- }
-
/**
* we need to check low replication in period, see HBASE-18132
*/
- void checkLowReplication(long now) {
+ private void checkLowReplication(long now) {
try {
for (Entry entry : walNeedsRoll.entrySet()) {
WAL wal = entry.getKey();
@@ -152,47 +150,49 @@ public class LogRoller extends HasThread implements Closeable {
@Override
public void run() {
while (running) {
+ boolean periodic = false;
long now = System.currentTimeMillis();
checkLowReplication(now);
- boolean periodic = false;
- if (!rollLog.get()) {
- periodic = (now - this.lastrolltime) > this.rollperiod;
- if (!periodic) {
- synchronized (rollLog) {
- try {
- if (!rollLog.get()) {
- rollLog.wait(this.threadWakeFrequency);
- }
- } catch (InterruptedException e) {
- // Fall through
- }
- }
- continue;
- }
- // Time for periodic roll
- LOG.debug("Wal roll period {} ms elapsed", this.rollperiod);
+ periodic = (now - this.lastRollTime) > this.rollPeriod;
+ if (periodic) {
+ // Time for periodic roll, fall through
+ LOG.debug("Wal roll period {} ms elapsed", this.rollPeriod);
} else {
- LOG.debug("WAL roll requested");
+ synchronized (this) {
+ if (walNeedsRoll.values().stream().anyMatch(Boolean::booleanValue)) {
+ // WAL roll requested, fall through
+ LOG.debug("WAL roll requested");
+ } else {
+ try {
+ wait(this.threadWakeFrequency);
+ } catch (InterruptedException e) {
+ // restore the interrupt state
+ Thread.currentThread().interrupt();
+ }
+ // goto the beginning to check whether again whether we should fall through to roll
+ // several WALs, and also check whether we should quit.
+ continue;
+ }
+ }
}
- rollLock.lock(); // FindBugs UL_UNRELEASED_LOCK_EXCEPTION_PATH
try {
- this.lastrolltime = now;
- for (Entry entry : walNeedsRoll.entrySet()) {
- final WAL wal = entry.getKey();
- // Force the roll if the logroll.period is elapsed or if a roll was requested.
- // The returned value is an array of actual region names.
- final byte [][] regionsToFlush = wal.rollWriter(periodic ||
- entry.getValue().booleanValue());
+ this.lastRollTime = System.currentTimeMillis();
+ for (Iterator> iter = walNeedsRoll.entrySet().iterator(); iter
+ .hasNext();) {
+ Entry entry = iter.next();
+ WAL wal = entry.getKey();
+ // reset the flag in front to avoid missing roll request before we return from rollWriter.
walNeedsRoll.put(wal, Boolean.FALSE);
+ // Force the roll if the logroll.period is elapsed or if a roll was requested.
+ // The returned value is an array of actual region names.
+ byte[][] regionsToFlush = wal.rollWriter(periodic || entry.getValue().booleanValue());
if (regionsToFlush != null) {
for (byte[] r : regionsToFlush) {
- scheduleFlush(r);
+ scheduleFlush(Bytes.toString(r));
}
}
}
- } catch (FailedLogCloseException e) {
- abort("Failed log close in log roller", e);
- } catch (java.net.ConnectException e) {
+ } catch (FailedLogCloseException | ConnectException e) {
abort("Failed log close in log roller", e);
} catch (IOException ex) {
// Abort if we get here. We probably won't recover an IOE. HBASE-1132
@@ -201,12 +201,6 @@ public class LogRoller extends HasThread implements Closeable {
} catch (Exception ex) {
LOG.error("Log rolling failed", ex);
abort("Log rolling failed", ex);
- } finally {
- try {
- rollLog.set(false);
- } finally {
- rollLock.unlock();
- }
}
}
LOG.info("LogRoller exiting.");
@@ -215,22 +209,20 @@ public class LogRoller extends HasThread implements Closeable {
/**
* @param encodedRegionName Encoded name of region to flush.
*/
- private void scheduleFlush(final byte [] encodedRegionName) {
- boolean scheduled = false;
- HRegion r = (HRegion) this.services.getRegion(Bytes.toString(encodedRegionName));
- FlushRequester requester = null;
- if (r != null) {
- requester = this.services.getFlushRequester();
- if (requester != null) {
- // force flushing all stores to clean old logs
- requester.requestFlush(r, true, FlushLifeCycleTracker.DUMMY);
- scheduled = true;
- }
+ private void scheduleFlush(String encodedRegionName) {
+ HRegion r = (HRegion) this.services.getRegion(encodedRegionName);
+ if (r == null) {
+ LOG.warn("Failed to schedule flush of {}, because it is not online on us", encodedRegionName);
+ return;
}
- if (!scheduled) {
- LOG.warn("Failed to schedule flush of {}, region={}, requester={}",
- Bytes.toString(encodedRegionName), r, requester);
+ FlushRequester requester = this.services.getFlushRequester();
+ if (requester == null) {
+ LOG.warn("Failed to schedule flush of {}, region={}, because FlushRequester is null",
+ encodedRegionName, r);
+ return;
}
+ // force flushing all stores to clean old logs
+ requester.requestFlush(r, true, FlushLifeCycleTracker.DUMMY);
}
/**
@@ -239,12 +231,7 @@ public class LogRoller extends HasThread implements Closeable {
*/
@VisibleForTesting
public boolean walRollFinished() {
- for (boolean needRoll : walNeedsRoll.values()) {
- if (needRoll) {
- return false;
- }
- }
- return true;
+ return walNeedsRoll.values().stream().allMatch(needRoll -> !needRoll);
}
@Override
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/wal/AbstractFSWAL.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/wal/AbstractFSWAL.java
index fbcc783f942..af7cc0d2026 100644
--- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/wal/AbstractFSWAL.java
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/wal/AbstractFSWAL.java
@@ -299,7 +299,7 @@ public abstract class AbstractFSWAL implements WAL {
*/
protected final String implClassName;
- protected volatile boolean rollRequested;
+ protected final AtomicBoolean rollRequested = new AtomicBoolean(false);
public long getFilenum() {
return this.filenum.get();
@@ -708,11 +708,8 @@ public abstract class AbstractFSWAL implements WAL {
}
/**
- *
- * Cleans up current writer closing it and then puts in place the passed in
- * nextWriter
.
- *
- *
+ * Cleans up current writer closing it and then puts in place the passed in {@code nextWriter}.
+ *
*
* - In the case of creating a new WAL, oldPath will be null.
* - In the case of rolling over from one file to the next, none of the parameters will be null.
@@ -720,7 +717,6 @@ public abstract class AbstractFSWAL implements WAL {
*
- In the case of closing out this FSHLog with no further use newPath and nextWriter will be
* null.
*
- *
* @param oldPath may be null
* @param newPath may be null
* @param nextWriter may be null
@@ -791,8 +787,6 @@ public abstract class AbstractFSWAL implements WAL {
tellListenersAboutPreLogRoll(oldPath, newPath);
// NewPath could be equal to oldPath if replaceWriter fails.
newPath = replaceWriter(oldPath, newPath, nextWriter);
- // Reset rollRequested status
- rollRequested = false;
tellListenersAboutPostLogRoll(oldPath, newPath);
if (LOG.isDebugEnabled()) {
LOG.debug("Create new " + implClassName + " writer with pipeline: " +
@@ -908,16 +902,13 @@ public abstract class AbstractFSWAL implements WAL {
}
protected boolean isLogRollRequested() {
- return rollRequested;
+ return rollRequested.get();
}
protected final void requestLogRoll(final WALActionsListener.RollRequestReason reason) {
// If we have already requested a roll, don't do it again
- if (rollRequested) {
- return;
- }
- if (!this.listeners.isEmpty()) {
- rollRequested = true; // No point to assert this unless there is a registered listener
+ // And only set rollRequested to true when there is a registered listener
+ if (!this.listeners.isEmpty() && rollRequested.compareAndSet(false, true)) {
for (WALActionsListener i : this.listeners) {
i.logRollRequested(reason);
}
@@ -1086,6 +1077,13 @@ public abstract class AbstractFSWAL implements WAL {
protected abstract W createWriterInstance(Path path)
throws IOException, CommonFSUtils.StreamLacksCapabilityException;
+ /**
+ * Notice that you need to clear the {@link #rollRequested} flag in this method, as the new writer
+ * will begin to work before returning from this method. If we clear the flag after returning from
+ * this call, we may miss a roll request. The implementation class should choose a proper place to
+ * clear the {@link #rollRequested} flag so we do not miss a roll request, typically before you
+ * start writing to the new writer.
+ */
protected abstract void doReplaceWriter(Path oldPath, Path newPath, W nextWriter)
throws IOException;
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/wal/AsyncFSWAL.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/wal/AsyncFSWAL.java
index 8482809df63..e12c03942b4 100644
--- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/wal/AsyncFSWAL.java
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/wal/AsyncFSWAL.java
@@ -676,6 +676,8 @@ public class AsyncFSWAL extends AbstractFSWAL {
int nextEpoch = currentEpoch == MAX_EPOCH ? 0 : currentEpoch + 1;
// set a new epoch and also clear waitingRoll and writerBroken
this.epochAndState = nextEpoch << 2;
+ // Reset rollRequested status
+ rollRequested.set(false);
consumeExecutor.execute(consumer);
} finally {
consumeLock.unlock();
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/wal/FSHLog.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/wal/FSHLog.java
index 6d1709c8c0d..9c6eafac43f 100644
--- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/wal/FSHLog.java
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/wal/FSHLog.java
@@ -386,6 +386,8 @@ public class FSHLog extends AbstractFSWAL {
} finally {
// Let the writer thread go regardless, whether error or not.
if (zigzagLatch != null) {
+ // Reset rollRequested status
+ rollRequested.set(false);
zigzagLatch.releaseSafePoint();
// syncFuture will be null if we failed our wait on safe point above. Otherwise, if
// latch was obtained successfully, the sync we threw in either trigger the latch or it
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestWALLockup.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestWALLockup.java
index 5c2bd06f010..1bf9c6f571d 100644
--- a/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestWALLockup.java
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestWALLockup.java
@@ -435,7 +435,7 @@ public class TestWALLockup {
// To stop logRoller, its server has to say it is stopped.
Mockito.when(server.isStopped()).thenReturn(true);
if (logRoller != null) {
- logRoller.interrupt();
+ logRoller.close();
}
if (dodgyWAL != null) {
dodgyWAL.close();
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/wal/TestAsyncFSWAL.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/wal/TestAsyncFSWAL.java
index 399cdc4162f..93024288e70 100644
--- a/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/wal/TestAsyncFSWAL.java
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/wal/TestAsyncFSWAL.java
@@ -17,18 +17,49 @@
*/
package org.apache.hadoop.hbase.regionserver.wal;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
import java.io.IOException;
+import java.io.UncheckedIOException;
import java.util.List;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.HBaseClassTestRule;
-import org.apache.hadoop.hbase.testclassification.MediumTests;
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.hadoop.hbase.KeyValue;
+import org.apache.hadoop.hbase.Server;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
+import org.apache.hadoop.hbase.client.RegionInfo;
+import org.apache.hadoop.hbase.client.RegionInfoBuilder;
+import org.apache.hadoop.hbase.client.TableDescriptor;
+import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
+import org.apache.hadoop.hbase.regionserver.LogRoller;
+import org.apache.hadoop.hbase.regionserver.MultiVersionConcurrencyControl;
+import org.apache.hadoop.hbase.regionserver.RegionServerServices;
+import org.apache.hadoop.hbase.regionserver.SequenceId;
+import org.apache.hadoop.hbase.testclassification.LargeTests;
import org.apache.hadoop.hbase.testclassification.RegionServerTests;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.hadoop.hbase.util.CommonFSUtils;
+import org.apache.hadoop.hbase.util.FutureUtils;
import org.apache.hadoop.hbase.util.Threads;
+import org.apache.hadoop.hbase.wal.WALEdit;
+import org.apache.hadoop.hbase.wal.WALKey;
+import org.apache.hadoop.hbase.wal.WALKeyImpl;
+import org.apache.hadoop.hbase.wal.WALProvider.AsyncWriter;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
+import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.apache.hbase.thirdparty.io.netty.channel.Channel;
@@ -39,7 +70,7 @@ import org.apache.hbase.thirdparty.io.netty.channel.socket.nio.NioSocketChannel;
/**
* Provides AsyncFSWAL test cases.
*/
-@Category({ RegionServerTests.class, MediumTests.class })
+@Category({ RegionServerTests.class, LargeTests.class })
public class TestAsyncFSWAL extends AbstractTestFSWAL {
@ClassRule
@@ -90,4 +121,101 @@ public class TestAsyncFSWAL extends AbstractTestFSWAL {
asyncFSWAL.init();
return asyncFSWAL;
}
+
+ @Test
+ public void testBrokenWriter() throws Exception {
+ Server server = mock(Server.class);
+ when(server.getConfiguration()).thenReturn(CONF);
+ RegionServerServices services = mock(RegionServerServices.class);
+ TableDescriptor td = TableDescriptorBuilder.newBuilder(TableName.valueOf("table"))
+ .setColumnFamily(ColumnFamilyDescriptorBuilder.of("row")).build();
+ RegionInfo ri = RegionInfoBuilder.newBuilder(td.getTableName()).build();
+ MultiVersionConcurrencyControl mvcc = new MultiVersionConcurrencyControl();
+ NavigableMap scopes = new TreeMap<>(Bytes.BYTES_COMPARATOR);
+ for (byte[] fam : td.getColumnFamilyNames()) {
+ scopes.put(fam, 0);
+ }
+ long timestamp = System.currentTimeMillis();
+ String testName = currentTest.getMethodName();
+ AtomicInteger failedCount = new AtomicInteger(0);
+ try (LogRoller roller = new LogRoller(server, services);
+ AsyncFSWAL wal = new AsyncFSWAL(FS, CommonFSUtils.getWALRootDir(CONF), DIR.toString(),
+ testName, CONF, null, true, null, null, GROUP, CHANNEL_CLASS) {
+
+ @Override
+ protected AsyncWriter createWriterInstance(Path path) throws IOException {
+ AsyncWriter writer = super.createWriterInstance(path);
+ return new AsyncWriter() {
+
+ @Override
+ public void close() throws IOException {
+ writer.close();
+ }
+
+ @Override
+ public long getLength() {
+ return writer.getLength();
+ }
+
+ @Override
+ public CompletableFuture sync() {
+ CompletableFuture result = writer.sync();
+ if (failedCount.incrementAndGet() < 1000) {
+ CompletableFuture future = new CompletableFuture<>();
+ FutureUtils.addListener(result,
+ (r, e) -> future.completeExceptionally(new IOException("Inject Error")));
+ return future;
+ } else {
+ return result;
+ }
+ }
+
+ @Override
+ public void append(Entry entry) {
+ writer.append(entry);
+ }
+ };
+ }
+ }) {
+ wal.init();
+ roller.addWAL(wal);
+ roller.start();
+ int numThreads = 10;
+ AtomicReference error = new AtomicReference<>();
+ Thread[] threads = new Thread[numThreads];
+ for (int i = 0; i < 10; i++) {
+ final int index = i;
+ threads[index] = new Thread("Write-Thread-" + index) {
+
+ @Override
+ public void run() {
+ byte[] row = Bytes.toBytes("row" + index);
+ WALEdit cols = new WALEdit();
+ cols.add(new KeyValue(row, row, row, timestamp + index, row));
+ WALKeyImpl key = new WALKeyImpl(ri.getEncodedNameAsBytes(), td.getTableName(),
+ SequenceId.NO_SEQUENCE_ID, timestamp, WALKey.EMPTY_UUIDS, HConstants.NO_NONCE,
+ HConstants.NO_NONCE, mvcc, scopes);
+ try {
+ wal.append(ri, key, cols, true);
+ } catch (IOException e) {
+ // should not happen
+ throw new UncheckedIOException(e);
+ }
+ try {
+ wal.sync();
+ } catch (IOException e) {
+ error.set(e);
+ }
+ }
+ };
+ }
+ for (Thread t : threads) {
+ t.start();
+ }
+ for (Thread t : threads) {
+ t.join();
+ }
+ assertNull(error.get());
+ }
+ }
}