HDFS-17148. RBF: SQLDelegationTokenSecretManager must cleanup expired tokens in SQL (#5936)
This commit is contained in:
parent
59f3a16819
commit
ad2f45c64f
@ -190,6 +190,14 @@ public long getCurrentTokensSize() {
|
|||||||
return currentTokens.size();
|
return currentTokens.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interval for tokens to be renewed.
|
||||||
|
* @return Renew interval in milliseconds.
|
||||||
|
*/
|
||||||
|
protected long getTokenRenewInterval() {
|
||||||
|
return this.tokenRenewInterval;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a previously used master key to cache (when NN restarts),
|
* Add a previously used master key to cache (when NN restarts),
|
||||||
* should be called before activate().
|
* should be called before activate().
|
||||||
@ -751,7 +759,7 @@ private void removeExpiredToken() throws IOException {
|
|||||||
Set<TokenIdent> expiredTokens = new HashSet<>();
|
Set<TokenIdent> expiredTokens = new HashSet<>();
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
Iterator<Map.Entry<TokenIdent, DelegationTokenInformation>> i =
|
Iterator<Map.Entry<TokenIdent, DelegationTokenInformation>> i =
|
||||||
currentTokens.entrySet().iterator();
|
getCandidateTokensForCleanup().entrySet().iterator();
|
||||||
while (i.hasNext()) {
|
while (i.hasNext()) {
|
||||||
Map.Entry<TokenIdent, DelegationTokenInformation> entry = i.next();
|
Map.Entry<TokenIdent, DelegationTokenInformation> entry = i.next();
|
||||||
long renewDate = entry.getValue().getRenewDate();
|
long renewDate = entry.getValue().getRenewDate();
|
||||||
@ -766,6 +774,10 @@ private void removeExpiredToken() throws IOException {
|
|||||||
logExpireTokens(expiredTokens);
|
logExpireTokens(expiredTokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Map<TokenIdent, DelegationTokenInformation> getCandidateTokensForCleanup() {
|
||||||
|
return this.currentTokens;
|
||||||
|
}
|
||||||
|
|
||||||
protected void logExpireTokens(
|
protected void logExpireTokens(
|
||||||
Collection<TokenIdent> expiredTokens) throws IOException {
|
Collection<TokenIdent> expiredTokens) throws IOException {
|
||||||
for (TokenIdent ident : expiredTokens) {
|
for (TokenIdent ident : expiredTokens) {
|
||||||
|
@ -24,6 +24,8 @@
|
|||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import org.apache.hadoop.classification.VisibleForTesting;
|
import org.apache.hadoop.classification.VisibleForTesting;
|
||||||
@ -50,6 +52,9 @@ public abstract class SQLDelegationTokenSecretManager<TokenIdent
|
|||||||
private static final String SQL_DTSM_TOKEN_SEQNUM_BATCH_SIZE = SQL_DTSM_CONF_PREFIX
|
private static final String SQL_DTSM_TOKEN_SEQNUM_BATCH_SIZE = SQL_DTSM_CONF_PREFIX
|
||||||
+ "token.seqnum.batch.size";
|
+ "token.seqnum.batch.size";
|
||||||
public static final int DEFAULT_SEQ_NUM_BATCH_SIZE = 10;
|
public static final int DEFAULT_SEQ_NUM_BATCH_SIZE = 10;
|
||||||
|
public static final String SQL_DTSM_TOKEN_MAX_CLEANUP_RESULTS = SQL_DTSM_CONF_PREFIX
|
||||||
|
+ "token.max.cleanup.results";
|
||||||
|
public static final int SQL_DTSM_TOKEN_MAX_CLEANUP_RESULTS_DEFAULT = 1000;
|
||||||
public static final String SQL_DTSM_TOKEN_LOADING_CACHE_EXPIRATION = SQL_DTSM_CONF_PREFIX
|
public static final String SQL_DTSM_TOKEN_LOADING_CACHE_EXPIRATION = SQL_DTSM_CONF_PREFIX
|
||||||
+ "token.loading.cache.expiration";
|
+ "token.loading.cache.expiration";
|
||||||
public static final long SQL_DTSM_TOKEN_LOADING_CACHE_EXPIRATION_DEFAULT =
|
public static final long SQL_DTSM_TOKEN_LOADING_CACHE_EXPIRATION_DEFAULT =
|
||||||
@ -63,6 +68,9 @@ public abstract class SQLDelegationTokenSecretManager<TokenIdent
|
|||||||
// exhausted, including during initialization.
|
// exhausted, including during initialization.
|
||||||
private final int seqNumBatchSize;
|
private final int seqNumBatchSize;
|
||||||
|
|
||||||
|
// Number of tokens to obtain from SQL during the cleanup process.
|
||||||
|
private final int maxTokenCleanupResults;
|
||||||
|
|
||||||
// Last sequenceNum in the current batch that has been allocated to a token.
|
// Last sequenceNum in the current batch that has been allocated to a token.
|
||||||
private int currentSeqNum;
|
private int currentSeqNum;
|
||||||
|
|
||||||
@ -82,6 +90,8 @@ public SQLDelegationTokenSecretManager(Configuration conf) {
|
|||||||
|
|
||||||
this.seqNumBatchSize = conf.getInt(SQL_DTSM_TOKEN_SEQNUM_BATCH_SIZE,
|
this.seqNumBatchSize = conf.getInt(SQL_DTSM_TOKEN_SEQNUM_BATCH_SIZE,
|
||||||
DEFAULT_SEQ_NUM_BATCH_SIZE);
|
DEFAULT_SEQ_NUM_BATCH_SIZE);
|
||||||
|
this.maxTokenCleanupResults = conf.getInt(SQL_DTSM_TOKEN_MAX_CLEANUP_RESULTS,
|
||||||
|
SQL_DTSM_TOKEN_MAX_CLEANUP_RESULTS_DEFAULT);
|
||||||
|
|
||||||
long cacheExpirationMs = conf.getTimeDuration(SQL_DTSM_TOKEN_LOADING_CACHE_EXPIRATION,
|
long cacheExpirationMs = conf.getTimeDuration(SQL_DTSM_TOKEN_LOADING_CACHE_EXPIRATION,
|
||||||
SQL_DTSM_TOKEN_LOADING_CACHE_EXPIRATION_DEFAULT, TimeUnit.MILLISECONDS);
|
SQL_DTSM_TOKEN_LOADING_CACHE_EXPIRATION_DEFAULT, TimeUnit.MILLISECONDS);
|
||||||
@ -153,6 +163,39 @@ public synchronized TokenIdent cancelToken(Token<TokenIdent> token,
|
|||||||
return super.cancelToken(token, canceller);
|
return super.cancelToken(token, canceller);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtain a list of tokens that will be considered for cleanup, based on the last
|
||||||
|
* time the token was updated in SQL. This list may include tokens that are not
|
||||||
|
* expired and should not be deleted (e.g. if the token was last renewed using a
|
||||||
|
* higher renewal interval).
|
||||||
|
* The number of results is limited to reduce performance impact. Some level of
|
||||||
|
* contention is expected when multiple routers run cleanup simultaneously.
|
||||||
|
* @return Map of tokens that have not been updated in SQL after the token renewal
|
||||||
|
* period.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected Map<TokenIdent, DelegationTokenInformation> getCandidateTokensForCleanup() {
|
||||||
|
Map<TokenIdent, DelegationTokenInformation> tokens = new HashMap<>();
|
||||||
|
try {
|
||||||
|
// Query SQL for tokens that haven't been updated after
|
||||||
|
// the last token renewal period.
|
||||||
|
long maxModifiedTime = Time.now() - getTokenRenewInterval();
|
||||||
|
Map<byte[], byte[]> tokenInfoBytesList = selectStaleTokenInfos(maxModifiedTime,
|
||||||
|
this.maxTokenCleanupResults);
|
||||||
|
|
||||||
|
LOG.info("Found {} tokens for cleanup", tokenInfoBytesList.size());
|
||||||
|
for (Map.Entry<byte[], byte[]> tokenInfoBytes : tokenInfoBytesList.entrySet()) {
|
||||||
|
TokenIdent tokenIdent = createTokenIdent(tokenInfoBytes.getKey());
|
||||||
|
DelegationTokenInformation tokenInfo = createTokenInfo(tokenInfoBytes.getValue());
|
||||||
|
tokens.put(tokenIdent, tokenInfo);
|
||||||
|
}
|
||||||
|
} catch (IOException | SQLException e) {
|
||||||
|
LOG.error("Failed to get candidate tokens for cleanup in SQL secret manager", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the existing TokenInformation from the SQL database to
|
* Removes the existing TokenInformation from the SQL database to
|
||||||
* invalidate it.
|
* invalidate it.
|
||||||
@ -415,6 +458,8 @@ public int incrementCurrentKeyId() {
|
|||||||
// Token operations in SQL database
|
// Token operations in SQL database
|
||||||
protected abstract byte[] selectTokenInfo(int sequenceNum, byte[] tokenIdentifier)
|
protected abstract byte[] selectTokenInfo(int sequenceNum, byte[] tokenIdentifier)
|
||||||
throws SQLException;
|
throws SQLException;
|
||||||
|
protected abstract Map<byte[], byte[]> selectStaleTokenInfos(long maxModifiedTime,
|
||||||
|
int maxResults) throws SQLException;
|
||||||
protected abstract void insertToken(int sequenceNum, byte[] tokenIdentifier, byte[] tokenInfo)
|
protected abstract void insertToken(int sequenceNum, byte[] tokenIdentifier, byte[] tokenInfo)
|
||||||
throws SQLException;
|
throws SQLException;
|
||||||
protected abstract void updateToken(int sequenceNum, byte[] tokenIdentifier, byte[] tokenInfo)
|
protected abstract void updateToken(int sequenceNum, byte[] tokenIdentifier, byte[] tokenInfo)
|
||||||
|
@ -23,6 +23,9 @@
|
|||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import org.apache.hadoop.classification.VisibleForTesting;
|
import org.apache.hadoop.classification.VisibleForTesting;
|
||||||
import org.apache.hadoop.conf.Configuration;
|
import org.apache.hadoop.conf.Configuration;
|
||||||
import org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifier;
|
import org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifier;
|
||||||
@ -150,6 +153,27 @@ protected byte[] selectTokenInfo(int sequenceNum, byte[] tokenIdentifier) throws
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Map<byte[], byte[]> selectStaleTokenInfos(long maxModifiedTime, int maxResults)
|
||||||
|
throws SQLException {
|
||||||
|
return retryHandler.execute(() -> {
|
||||||
|
try (Connection connection = connectionFactory.getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(
|
||||||
|
"SELECT tokenIdentifier, tokenInfo FROM Tokens WHERE modifiedTime < ?")) {
|
||||||
|
statement.setTimestamp(1, new Timestamp(maxModifiedTime));
|
||||||
|
statement.setMaxRows(maxResults);
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
Map<byte[], byte[]> results = new HashMap<>();
|
||||||
|
while (result.next()) {
|
||||||
|
results.put(result.getBytes("tokenIdentifier"),
|
||||||
|
result.getBytes("tokenInfo"));
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void insertDelegationKey(int keyId, byte[] delegationKey) throws SQLException {
|
protected void insertDelegationKey(int keyId, byte[] delegationKey) throws SQLException {
|
||||||
retryHandler.execute(() -> {
|
retryHandler.execute(() -> {
|
||||||
|
@ -30,12 +30,15 @@
|
|||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import org.apache.hadoop.conf.Configuration;
|
import org.apache.hadoop.conf.Configuration;
|
||||||
|
import org.apache.hadoop.io.Text;
|
||||||
import org.apache.hadoop.security.UserGroupInformation;
|
import org.apache.hadoop.security.UserGroupInformation;
|
||||||
import org.apache.hadoop.security.token.SecretManager;
|
import org.apache.hadoop.security.token.SecretManager;
|
||||||
import org.apache.hadoop.security.token.Token;
|
import org.apache.hadoop.security.token.Token;
|
||||||
import org.apache.hadoop.security.token.TokenIdentifier;
|
import org.apache.hadoop.security.token.TokenIdentifier;
|
||||||
import org.apache.hadoop.security.token.delegation.AbstractDelegationTokenIdentifier;
|
import org.apache.hadoop.security.token.delegation.AbstractDelegationTokenIdentifier;
|
||||||
|
import org.apache.hadoop.security.token.delegation.AbstractDelegationTokenSecretManager;
|
||||||
import org.apache.hadoop.security.token.delegation.SQLDelegationTokenSecretManager;
|
import org.apache.hadoop.security.token.delegation.SQLDelegationTokenSecretManager;
|
||||||
|
import org.apache.hadoop.security.token.delegation.web.DelegationTokenIdentifier;
|
||||||
import org.apache.hadoop.security.token.delegation.web.DelegationTokenManager;
|
import org.apache.hadoop.security.token.delegation.web.DelegationTokenManager;
|
||||||
import org.apache.hadoop.test.GenericTestUtils;
|
import org.apache.hadoop.test.GenericTestUtils;
|
||||||
import org.apache.hadoop.test.LambdaTestUtils;
|
import org.apache.hadoop.test.LambdaTestUtils;
|
||||||
@ -52,6 +55,7 @@ public class TestSQLDelegationTokenSecretManagerImpl {
|
|||||||
private static final String CONNECTION_URL = "jdbc:derby:memory:TokenStore";
|
private static final String CONNECTION_URL = "jdbc:derby:memory:TokenStore";
|
||||||
private static final int TEST_MAX_RETRIES = 3;
|
private static final int TEST_MAX_RETRIES = 3;
|
||||||
private static final int TOKEN_EXPIRATION_SECONDS = 1;
|
private static final int TOKEN_EXPIRATION_SECONDS = 1;
|
||||||
|
private static final int TOKEN_EXPIRATION_SCAN_SECONDS = 1;
|
||||||
private static Configuration conf;
|
private static Configuration conf;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@ -75,6 +79,7 @@ public static void initDatabase() throws SQLException {
|
|||||||
conf.set(SQLConnectionFactory.CONNECTION_DRIVER, "org.apache.derby.jdbc.EmbeddedDriver");
|
conf.set(SQLConnectionFactory.CONNECTION_DRIVER, "org.apache.derby.jdbc.EmbeddedDriver");
|
||||||
conf.setInt(SQLSecretManagerRetriableHandlerImpl.MAX_RETRIES, TEST_MAX_RETRIES);
|
conf.setInt(SQLSecretManagerRetriableHandlerImpl.MAX_RETRIES, TEST_MAX_RETRIES);
|
||||||
conf.setInt(SQLSecretManagerRetriableHandlerImpl.RETRY_SLEEP_TIME_MS, 10);
|
conf.setInt(SQLSecretManagerRetriableHandlerImpl.RETRY_SLEEP_TIME_MS, 10);
|
||||||
|
conf.setInt(DelegationTokenManager.REMOVAL_SCAN_INTERVAL, TOKEN_EXPIRATION_SCAN_SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterClass
|
@AfterClass
|
||||||
@ -188,6 +193,62 @@ public void testRenewToken() throws Exception {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRemoveExpiredTokens() throws Exception {
|
||||||
|
DelegationTokenManager tokenManager = createTokenManager(getShortLivedTokenConf());
|
||||||
|
|
||||||
|
try {
|
||||||
|
TestDelegationTokenSecretManager secretManager =
|
||||||
|
(TestDelegationTokenSecretManager) tokenManager.getDelegationTokenSecretManager();
|
||||||
|
|
||||||
|
// Create token to be constantly renewed.
|
||||||
|
Token token1 = tokenManager.createToken(UserGroupInformation.getCurrentUser(), "foo");
|
||||||
|
AbstractDelegationTokenIdentifier tokenId1 =
|
||||||
|
(AbstractDelegationTokenIdentifier) token1.decodeIdentifier();
|
||||||
|
|
||||||
|
// Create token expected to expire soon.
|
||||||
|
long expirationTime2 = Time.now();
|
||||||
|
AbstractDelegationTokenIdentifier tokenId2 = storeToken(secretManager, 2, expirationTime2);
|
||||||
|
|
||||||
|
// Create token not expected to expire soon.
|
||||||
|
long expirationTime3 = Time.now() + TimeUnit.SECONDS.toMillis(TOKEN_EXPIRATION_SECONDS) * 10;
|
||||||
|
AbstractDelegationTokenIdentifier tokenId3 = storeToken(secretManager, 3, expirationTime3);
|
||||||
|
|
||||||
|
GenericTestUtils.waitFor(() -> {
|
||||||
|
try {
|
||||||
|
// Constantly renew token so it doesn't expire.
|
||||||
|
tokenManager.renewToken(token1, "foo");
|
||||||
|
|
||||||
|
// Wait for cleanup to happen so expired token is deleted from SQL.
|
||||||
|
return !isTokenInSQL(secretManager, tokenId2);
|
||||||
|
} catch (IOException | SQLException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}, 100, 6000);
|
||||||
|
|
||||||
|
Assert.assertTrue("Renewed token must not be cleaned up",
|
||||||
|
isTokenInSQL(secretManager, tokenId1));
|
||||||
|
Assert.assertTrue("Token with future expiration must not be cleaned up",
|
||||||
|
isTokenInSQL(secretManager, tokenId3));
|
||||||
|
} finally {
|
||||||
|
stopTokenManager(tokenManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AbstractDelegationTokenIdentifier storeToken(
|
||||||
|
TestDelegationTokenSecretManager secretManager, int sequenceNum, long expirationTime)
|
||||||
|
throws IOException {
|
||||||
|
AbstractDelegationTokenIdentifier tokenId = new DelegationTokenIdentifier(new Text("Test"));
|
||||||
|
tokenId.setOwner(new Text("foo"));
|
||||||
|
tokenId.setSequenceNumber(sequenceNum);
|
||||||
|
|
||||||
|
AbstractDelegationTokenSecretManager.DelegationTokenInformation tokenInfo =
|
||||||
|
new AbstractDelegationTokenSecretManager.DelegationTokenInformation(expirationTime, null);
|
||||||
|
secretManager.storeToken(tokenId, tokenInfo);
|
||||||
|
|
||||||
|
return tokenId;
|
||||||
|
}
|
||||||
|
|
||||||
private Configuration getShortLivedTokenConf() {
|
private Configuration getShortLivedTokenConf() {
|
||||||
Configuration shortLivedConf = new Configuration(conf);
|
Configuration shortLivedConf = new Configuration(conf);
|
||||||
shortLivedConf.setTimeDuration(
|
shortLivedConf.setTimeDuration(
|
||||||
@ -201,13 +262,12 @@ private void callRemoveExpiredTokensAndValidateSQL(
|
|||||||
TestDelegationTokenSecretManager secretManager, AbstractDelegationTokenIdentifier tokenId,
|
TestDelegationTokenSecretManager secretManager, AbstractDelegationTokenIdentifier tokenId,
|
||||||
boolean expectedInSQL) throws SQLException {
|
boolean expectedInSQL) throws SQLException {
|
||||||
secretManager.removeExpiredStoredToken(tokenId);
|
secretManager.removeExpiredStoredToken(tokenId);
|
||||||
byte[] tokenInfo = secretManager.selectTokenInfo(tokenId.getSequenceNumber(),
|
Assert.assertEquals(expectedInSQL, isTokenInSQL(secretManager, tokenId));
|
||||||
tokenId.getBytes());
|
}
|
||||||
if (expectedInSQL) {
|
|
||||||
Assert.assertNotNull("Verify token exists in database", tokenInfo);
|
private boolean isTokenInSQL(TestDelegationTokenSecretManager secretManager,
|
||||||
} else {
|
AbstractDelegationTokenIdentifier tokenId) throws SQLException {
|
||||||
Assert.assertNull("Verify token was removed from database", tokenInfo);
|
return secretManager.selectTokenInfo(tokenId.getSequenceNumber(), tokenId.getBytes()) != null;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -542,6 +602,11 @@ public void removeExpiredStoredToken(TokenIdentifier tokenId) {
|
|||||||
super.removeExpiredStoredToken((AbstractDelegationTokenIdentifier) tokenId);
|
super.removeExpiredStoredToken((AbstractDelegationTokenIdentifier) tokenId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void storeToken(AbstractDelegationTokenIdentifier ident,
|
||||||
|
DelegationTokenInformation tokenInfo) throws IOException {
|
||||||
|
super.storeToken(ident, tokenInfo);
|
||||||
|
}
|
||||||
|
|
||||||
public void setReadOnly(boolean readOnly) {
|
public void setReadOnly(boolean readOnly) {
|
||||||
((TestConnectionFactory) getConnectionFactory()).readOnly = readOnly;
|
((TestConnectionFactory) getConnectionFactory()).readOnly = readOnly;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user