Skip to content

Commit 3836135

Browse files
Refactor Token Caching with CachedTokenSource Wrapper (#461)
## Changes This PR refactors the token refresh and caching in the Java SDK by: - Removing the `RefreshableTokenSource` class that previously handled token refresh and caching logic directly - Introducing `CachedTokenSource` as a generic wrapper that can be applied to any TokenSource implementation - Separating concerns by moving caching and async logic out of individual token sources into a dedicated cache wrapper ## Motivation Previously, token sources inherited from `RefreshableTokenSource` to gain caching and asynchronous refresh capabilities. However, this inheritance-based approach had limitations when we wanted to make configuration immutable after construction using the builder pattern. **The Problem**: With abstract classes in Java, constructors are not inherited. To support builder-based configuration, each token source subclass would need to be modified to accept a builder instead of individual parameters, requiring changes across many classes. **The Solution**: Switch from inheritance to composition. Instead of inheriting caching behavior, we now wrap any `TokenSource` which previously inherited from `RefreshableTokenSource` with `CachedTokenSource` to add caching functionality externally. This allows us to use the builder pattern to configure optional parameters like async refresh and stale duration on each wrapped instance. ## How is this tested? This change has been tested with async enabled/disabled by default on existing unit and integration tests. NO_CHANGELOG=true --------- Co-authored-by: Parth Bansal <[email protected]>
1 parent 66d514e commit 3836135

24 files changed

+373
-253
lines changed

databricks-sdk-java/src/main/java/com/databricks/sdk/core/AzureCliCredentialsProvider.java

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.databricks.sdk.core;
22

3+
import com.databricks.sdk.core.oauth.CachedTokenSource;
34
import com.databricks.sdk.core.oauth.OAuthHeaderFactory;
45
import com.databricks.sdk.core.oauth.Token;
56
import com.databricks.sdk.core.utils.AzureUtils;
@@ -19,7 +20,7 @@ public String authType() {
1920
return AZURE_CLI;
2021
}
2122

22-
public CliTokenSource tokenSourceFor(DatabricksConfig config, String resource) {
23+
public CachedTokenSource tokenSourceFor(DatabricksConfig config, String resource) {
2324
String azPath =
2425
Optional.ofNullable(config.getEnv()).map(env -> env.get("AZ_PATH")).orElse("az");
2526

@@ -35,7 +36,7 @@ public CliTokenSource tokenSourceFor(DatabricksConfig config, String resource) {
3536
List<String> extendedCmd = new ArrayList<>(cmd);
3637
extendedCmd.addAll(Arrays.asList("--subscription", subscription.get()));
3738
try {
38-
return getToken(config, extendedCmd);
39+
return getTokenSource(config, extendedCmd);
3940
} catch (DatabricksException ex) {
4041
LOG.warn("Failed to get token for subscription. Using resource only token.");
4142
}
@@ -45,14 +46,15 @@ public CliTokenSource tokenSourceFor(DatabricksConfig config, String resource) {
4546
+ "It is recommended to specify this field in the Databricks configuration to avoid authentication errors.");
4647
}
4748

48-
return getToken(config, cmd);
49+
return getTokenSource(config, cmd);
4950
}
5051

51-
protected CliTokenSource getToken(DatabricksConfig config, List<String> cmd) {
52-
CliTokenSource token =
52+
protected CachedTokenSource getTokenSource(DatabricksConfig config, List<String> cmd) {
53+
CliTokenSource tokenSource =
5354
new CliTokenSource(cmd, "tokenType", "accessToken", "expiresOn", config.getEnv());
54-
token.getToken(); // We need this to check if the CLI is installed and to validate the config.
55-
return token;
55+
CachedTokenSource cachedTokenSource = new CachedTokenSource.Builder(tokenSource).build();
56+
cachedTokenSource.getToken(); // Check if the CLI is installed and to validate the config.
57+
return cachedTokenSource;
5658
}
5759

5860
private Optional<String> getSubscription(DatabricksConfig config) {
@@ -77,16 +79,16 @@ public OAuthHeaderFactory configure(DatabricksConfig config) {
7779
try {
7880
AzureUtils.ensureHostPresent(config, mapper, this::tokenSourceFor);
7981
String resource = config.getEffectiveAzureLoginAppId();
80-
CliTokenSource tokenSource = tokenSourceFor(config, resource);
81-
CliTokenSource mgmtTokenSource;
82+
CachedTokenSource tokenSource = tokenSourceFor(config, resource);
83+
CachedTokenSource mgmtTokenSource;
8284
try {
8385
mgmtTokenSource =
8486
tokenSourceFor(config, config.getAzureEnvironment().getServiceManagementEndpoint());
8587
} catch (Exception e) {
8688
LOG.debug("Not including service management token in headers", e);
8789
mgmtTokenSource = null;
8890
}
89-
CliTokenSource finalMgmtTokenSource = mgmtTokenSource;
91+
CachedTokenSource finalMgmtTokenSource = mgmtTokenSource;
9092
return OAuthHeaderFactory.fromSuppliers(
9193
tokenSource::getToken,
9294
() -> {

databricks-sdk-java/src/main/java/com/databricks/sdk/core/CliTokenSource.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.databricks.sdk.core;
22

3-
import com.databricks.sdk.core.oauth.RefreshableTokenSource;
43
import com.databricks.sdk.core.oauth.Token;
4+
import com.databricks.sdk.core.oauth.TokenSource;
55
import com.databricks.sdk.core.utils.Environment;
66
import com.databricks.sdk.core.utils.OSUtils;
77
import com.fasterxml.jackson.databind.JsonNode;
@@ -18,7 +18,7 @@
1818
import java.util.List;
1919
import org.apache.commons.io.IOUtils;
2020

21-
public class CliTokenSource extends RefreshableTokenSource {
21+
public class CliTokenSource implements TokenSource {
2222
private List<String> cmd;
2323
private String tokenTypeField;
2424
private String accessTokenField;
@@ -86,7 +86,7 @@ private String getProcessStream(InputStream stream) throws IOException {
8686
}
8787

8888
@Override
89-
protected Token refresh() {
89+
public Token getToken() {
9090
try {
9191
ProcessBuilder processBuilder = new ProcessBuilder(cmd);
9292
processBuilder.environment().putAll(env.getEnv());

databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksCliCredentialsProvider.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.databricks.sdk.core;
22

3+
import com.databricks.sdk.core.oauth.CachedTokenSource;
34
import com.databricks.sdk.core.oauth.OAuthHeaderFactory;
45
import com.databricks.sdk.core.utils.OSUtils;
56
import java.util.*;
@@ -47,8 +48,11 @@ public OAuthHeaderFactory configure(DatabricksConfig config) {
4748
if (tokenSource == null) {
4849
return null;
4950
}
50-
tokenSource.getToken(); // We need this for checking if databricks CLI is installed.
51-
return OAuthHeaderFactory.fromTokenSource(tokenSource);
51+
52+
CachedTokenSource cachedTokenSource = new CachedTokenSource.Builder(tokenSource).build();
53+
cachedTokenSource.getToken(); // We need this for checking if databricks CLI is installed.
54+
55+
return OAuthHeaderFactory.fromTokenSource(cachedTokenSource);
5256
} catch (DatabricksException e) {
5357
String stderr = e.getMessage();
5458
if (stderr.contains("not found")) {

databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureGithubOidcCredentialsProvider.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ public OAuthHeaderFactory configure(DatabricksConfig config) {
4646
config.getEffectiveAzureLoginAppId(),
4747
idToken.get(),
4848
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
49-
50-
return OAuthHeaderFactory.fromTokenSource(tokenSource);
49+
CachedTokenSource cachedTokenSource = new CachedTokenSource.Builder(tokenSource).build();
50+
return OAuthHeaderFactory.fromTokenSource(cachedTokenSource);
5151
}
5252

5353
/**

databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureServicePrincipalCredentialsProvider.java

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ public OAuthHeaderFactory configure(DatabricksConfig config) {
2828
}
2929
AzureUtils.ensureHostPresent(
3030
config, mapper, AzureServicePrincipalCredentialsProvider::tokenSourceFor);
31-
RefreshableTokenSource inner = tokenSourceFor(config, config.getEffectiveAzureLoginAppId());
32-
RefreshableTokenSource cloud =
31+
CachedTokenSource inner = tokenSourceFor(config, config.getEffectiveAzureLoginAppId());
32+
CachedTokenSource cloud =
3333
tokenSourceFor(config, config.getAzureEnvironment().getServiceManagementEndpoint());
3434

3535
return OAuthHeaderFactory.fromSuppliers(
@@ -44,29 +44,32 @@ public OAuthHeaderFactory configure(DatabricksConfig config) {
4444
}
4545

4646
/**
47-
* Creates a RefreshableTokenSource for the specified Azure resource.
47+
* Creates a CachedTokenSource for the specified Azure resource.
4848
*
49-
* <p>This function constructs a RefreshableTokenSource instance that fetches OAuth tokens for the
49+
* <p>This function constructs a CachedTokenSource instance that fetches OAuth tokens for the
5050
* given Azure resource. It uses the authentication parameters provided by the DatabricksConfig
5151
* instance to generate the tokens.
5252
*
5353
* @param config The DatabricksConfig instance containing the required authentication parameters.
5454
* @param resource The Azure resource for which OAuth tokens need to be fetched.
55-
* @return A RefreshableTokenSource instance capable of fetching OAuth tokens for the specified
56-
* Azure resource.
55+
* @return A CachedTokenSource instance capable of fetching OAuth tokens for the specified Azure
56+
* resource.
5757
*/
58-
private static RefreshableTokenSource tokenSourceFor(DatabricksConfig config, String resource) {
58+
private static CachedTokenSource tokenSourceFor(DatabricksConfig config, String resource) {
5959
String aadEndpoint = config.getAzureEnvironment().getActiveDirectoryEndpoint();
6060
String tokenUrl = aadEndpoint + config.getAzureTenantId() + "/oauth2/token";
6161
Map<String, String> endpointParams = new HashMap<>();
6262
endpointParams.put("resource", resource);
63-
return new ClientCredentials.Builder()
64-
.withHttpClient(config.getHttpClient())
65-
.withClientId(config.getAzureClientId())
66-
.withClientSecret(config.getAzureClientSecret())
67-
.withTokenUrl(tokenUrl)
68-
.withEndpointParametersSupplier(() -> endpointParams)
69-
.withAuthParameterPosition(AuthParameterPosition.BODY)
70-
.build();
63+
64+
ClientCredentials clientCredentials =
65+
new ClientCredentials.Builder()
66+
.withHttpClient(config.getHttpClient())
67+
.withClientId(config.getAzureClientId())
68+
.withClientSecret(config.getAzureClientSecret())
69+
.withTokenUrl(tokenUrl)
70+
.withEndpointParametersSupplier(() -> endpointParams)
71+
.withAuthParameterPosition(AuthParameterPosition.BODY)
72+
.build();
73+
return new CachedTokenSource.Builder(clientCredentials).build();
7174
}
7275
}

0 commit comments

Comments
 (0)