Skip to content

Commit e01c72e

Browse files
committed
HBASE-30355 setupMiniKdc port-conflict retry never triggers because Kerby throws KrbException, not BindException
The retry-on-port-conflict block in setupMiniKdc only catches java.net.BindException, but the Kerby-backed MiniKdc wraps the bind failure in a KrbException with no BindException in its cause chain - the conflict is only visible via the "Address already in use" message. The retry was therefore dead code and the first port collision failed the test. Recognise the bind conflict regardless of wrapper type: catch Exception, and before retrying, test a predicate that walks the whole cause chain and matches the "Address already in use" message; rethrow anything else so a genuine KDC misconfig is not masked. We avoid catch (BindException | KrbException) because the message (not the type) is the discriminator and to avoid importing kerby types here (see HBASE-29117). Applied to both TestLogLevel.setupMiniKdc (hbase-http) and HBaseTestingUtil.setupMiniKdc (hbase-server). Tests: - testKdcBindConflictSurfacesAsKrbException: deterministic reproduction occupying the KDC port on TCP+UDP; asserts the failure is a KrbException the predicate recognises. - testIsBindExceptionRecognizesKerbyWrappedBindFailure: unit test for the predicate (wrapped BindException, message-only, and negative case).
1 parent c5bd5c5 commit e01c72e

2 files changed

Lines changed: 109 additions & 4 deletions

File tree

‎hbase-http/src/test/java/org/apache/hadoop/hbase/http/log/TestLogLevel.java‎

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,16 @@
2121
import static org.junit.jupiter.api.Assertions.assertFalse;
2222
import static org.junit.jupiter.api.Assertions.assertNotEquals;
2323
import static org.junit.jupiter.api.Assertions.assertSame;
24+
import static org.junit.jupiter.api.Assertions.assertThrows;
2425
import static org.junit.jupiter.api.Assertions.assertTrue;
2526
import static org.junit.jupiter.api.Assertions.fail;
2627

2728
import java.io.File;
2829
import java.io.IOException;
2930
import java.net.BindException;
31+
import java.net.DatagramSocket;
32+
import java.net.InetAddress;
33+
import java.net.ServerSocket;
3034
import java.net.SocketException;
3135
import java.net.URI;
3236
import java.security.PrivilegedExceptionAction;
@@ -153,20 +157,96 @@ static private MiniKdc setupMiniKdc() throws Exception {
153157
dir = new File(HTU.getDataTestDir("kdc").toUri().getPath());
154158
kdc = new MiniKdc(conf, dir);
155159
kdc.start();
156-
} catch (BindException e) {
160+
} catch (Exception e) {
161+
// Catch Exception, not BindException/KrbException: Kerby wraps the bind failure in a
162+
// KrbException (and the cause chain carries no BindException), so the port conflict is only
163+
// recognisable by message. We also avoid importing kerby types here (see HBASE-29117).
164+
if (!isBindException(e)) {
165+
throw e; // not a port conflict, do not mask the real failure behind a retry
166+
}
157167
FileUtils.deleteDirectory(dir); // clean directory
158168
numTries++;
159169
if (numTries == 3) {
160170
log.error("Failed setting up MiniKDC. Tried " + numTries + " times.");
161171
throw e;
162172
}
163-
log.error("BindException encountered when setting up MiniKdc. Trying again.");
173+
log.error("Bind conflict encountered when setting up MiniKdc, retrying (attempt " + numTries
174+
+ ").");
164175
bindException = true;
165176
}
166177
} while (bindException);
167178
return kdc;
168179
}
169180

181+
/**
182+
* The Kerby-backed {@link MiniKdc} wraps a failure to bind the KDC port in a
183+
* {@code org.apache.kerby...KrbException} rather than surfacing a {@link BindException} directly,
184+
* so we inspect the whole cause chain plus the message to recognise a port conflict.
185+
*/
186+
static boolean isBindException(Throwable t) {
187+
for (Throwable cause = t; cause != null; cause = cause.getCause()) {
188+
if (cause instanceof BindException) {
189+
return true;
190+
}
191+
String msg = cause.getMessage();
192+
if (msg != null && msg.contains("Address already in use")) {
193+
return true;
194+
}
195+
}
196+
return false;
197+
}
198+
199+
/**
200+
* Reproduces the port-conflict flake deterministically: occupy the KDC port on both TCP and UDP,
201+
* pin MiniKdc to it, and show the resulting failure is a Kerby {@code KrbException}, not a
202+
* {@link BindException}. This is why the {@code catch (BindException)} retry in
203+
* {@link #setupMiniKdc()} never fired for the reported failure.
204+
*/
205+
@Test
206+
public void testKdcBindConflictSurfacesAsKrbException() throws Exception {
207+
int port;
208+
try (ServerSocket probe = new ServerSocket(0, 1, InetAddress.getByName(LOCALHOST))) {
209+
port = probe.getLocalPort();
210+
}
211+
try (ServerSocket tcp = new ServerSocket(port, 1, InetAddress.getByName(LOCALHOST));
212+
DatagramSocket udp = new DatagramSocket(port, InetAddress.getByName(LOCALHOST))) {
213+
Properties conf = MiniKdc.createConf();
214+
conf.put(MiniKdc.DEBUG, true);
215+
conf.setProperty(MiniKdc.KDC_BIND_ADDRESS, LOCALHOST);
216+
conf.setProperty(MiniKdc.KDC_PORT, Integer.toString(port));
217+
File dir = new File(HTU.getDataTestDir("kdc-conflict").toUri().getPath());
218+
MiniKdc conflictingKdc = new MiniKdc(conf, dir);
219+
220+
Exception thrown = assertThrows(Exception.class, conflictingKdc::start);
221+
222+
assertFalse(thrown instanceof BindException,
223+
"Kerby wraps the bind failure in a KrbException, so it is not a BindException: " + thrown);
224+
assertTrue(isBindException(thrown),
225+
"expected a recognisable bind-conflict failure, got: " + thrown);
226+
}
227+
}
228+
229+
/**
230+
* Deterministic regression test for the retry predicate. A port conflict raised by the
231+
* Kerby-backed {@link MiniKdc} must be recognised as a bind failure whether the
232+
* {@link BindException} is preserved in the cause chain or only reflected in the message;
233+
* unrelated failures must not be. We model the Kerby wrapper with a plain {@link Exception}
234+
* rather than constructing a real Kerby exception, so the test does not import kerby types (see
235+
* HBASE-29117).
236+
*/
237+
@Test
238+
public void testIsBindExceptionRecognizesKerbyWrappedBindFailure() {
239+
assertTrue(
240+
isBindException(new Exception("Failed to start DefaultKrbServer",
241+
new BindException("Address already in use"))),
242+
"a bind failure preserved in the cause chain should be recognised");
243+
assertTrue(
244+
isBindException(new Exception("Failed to start DefaultKrbServer. Address already in use")),
245+
"a wrapper whose message reports the bind conflict should be recognised");
246+
assertFalse(isBindException(new RuntimeException("boom")),
247+
"an unrelated failure must not be treated as a retryable bind conflict");
248+
}
249+
170250
static private void setupSSL(File base) throws Exception {
171251
clientConf.set(DFSConfigKeys.DFS_HTTP_POLICY_KEY, HttpConfig.Policy.HTTPS_ONLY.name());
172252
clientConf.set(DFSConfigKeys.DFS_NAMENODE_HTTPS_ADDRESS_KEY, "localhost:0");

‎hbase-server/src/test/java/org/apache/hadoop/hbase/HBaseTestingUtil.java‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3731,21 +3731,46 @@ public MiniKdc setupMiniKdc(File keytabFile) throws Exception {
37313731
dir = new File(getDataTestDir("kdc").toUri().getPath());
37323732
kdc = new MiniKdc(conf, dir);
37333733
kdc.start();
3734-
} catch (BindException e) {
3734+
} catch (Exception e) {
3735+
// Catch Exception, not BindException/KrbException: Kerby wraps the bind failure in a
3736+
// KrbException (and the cause chain carries no BindException), so the port conflict is only
3737+
// recognisable by message. We also avoid importing kerby types here (see HBASE-29117).
3738+
if (!isBindException(e)) {
3739+
throw e; // not a port conflict, do not mask the real failure behind a retry
3740+
}
37353741
FileUtils.deleteDirectory(dir); // clean directory
37363742
numTries++;
37373743
if (numTries == 3) {
37383744
LOG.error("Failed setting up MiniKDC. Tried " + numTries + " times.");
37393745
throw e;
37403746
}
3741-
LOG.error("BindException encountered when setting up MiniKdc. Trying again.");
3747+
LOG.error("Bind conflict encountered when setting up MiniKdc, retrying (attempt " + numTries
3748+
+ ").");
37423749
bindException = true;
37433750
}
37443751
} while (bindException);
37453752
HBaseKerberosUtils.setKeytabFileForTesting(keytabFile.getAbsolutePath());
37463753
return kdc;
37473754
}
37483755

3756+
/**
3757+
* The Kerby-backed {@link MiniKdc} wraps a failure to bind the KDC port in a
3758+
* {@code org.apache.kerby...KrbException} rather than surfacing a {@link BindException} directly,
3759+
* so we inspect the whole cause chain plus the message to recognise a port conflict.
3760+
*/
3761+
static boolean isBindException(Throwable t) {
3762+
for (Throwable cause = t; cause != null; cause = cause.getCause()) {
3763+
if (cause instanceof BindException) {
3764+
return true;
3765+
}
3766+
String msg = cause.getMessage();
3767+
if (msg != null && msg.contains("Address already in use")) {
3768+
return true;
3769+
}
3770+
}
3771+
return false;
3772+
}
3773+
37493774
public int getNumHFiles(final TableName tableName, final byte[] family) {
37503775
int numHFiles = 0;
37513776
for (RegionServerThread regionServerThread : getMiniHBaseCluster().getRegionServerThreads()) {

0 commit comments

Comments
 (0)