Skip to content

Commit c082f09

Browse files
committed
Introduce DataSourceResolver for multi-datasource support in JDBC persistence
This commit introduces the DataSourceResolver interface and a DefaultDataSourceResolver implementation to enable flexible routing of DataSources based on realm and store type (e.g., main, metrics, events). Key changes: - New DataSourceResolver interface with resolve(realmId, storeType) and getAllUniqueDataSources() methods - DefaultDataSourceResolver that routes all requests to the default DataSource (backward-compatible no-op) - Refactored JdbcMetaStoreManagerFactory to use DataSourceResolver instead of direct DataSource injection This lays the groundwork for isolating different persistence workloads (entity metadata vs metrics vs events) into separate connection pools or databases to mitigate noisy neighbor effects. Closes #3890
1 parent b476779 commit c082f09

3 files changed

Lines changed: 173 additions & 45 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.polaris.persistence.relational.jdbc;
20+
21+
import java.util.Set;
22+
import javax.sql.DataSource;
23+
24+
/**
25+
* Service to resolve the correct {@link DataSource} for a given realm and store
26+
* type.
27+
* This enables isolating different workloads (e.g., entity metadata vs metrics
28+
* vs events)
29+
* into different physical databases or connection pools.
30+
*/
31+
public interface DataSourceResolver {
32+
33+
String STORE_TYPE_MAIN = "main";
34+
String STORE_TYPE_METRICS = "metrics";
35+
String STORE_TYPE_EVENTS = "events";
36+
37+
/**
38+
* Resolves the DataSource for a given realm and store type.
39+
*
40+
* @param realmId the realm identifier
41+
* @param storeType the type of store (e.g., main, metrics, events)
42+
* @return the resolved DataSource
43+
*/
44+
DataSource resolve(String realmId, String storeType);
45+
46+
/**
47+
* Returns all unique DataSources managed by this resolver.
48+
* This is useful for global operations like schema initialization against all
49+
* data sources.
50+
*
51+
* @return a set of all DataSources
52+
*/
53+
Set<DataSource> getAllUniqueDataSources();
54+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.polaris.persistence.relational.jdbc;
20+
21+
import jakarta.enterprise.context.ApplicationScoped;
22+
import jakarta.enterprise.inject.Instance;
23+
import jakarta.inject.Inject;
24+
import java.util.HashSet;
25+
import java.util.Set;
26+
import javax.sql.DataSource;
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
29+
30+
/**
31+
* Default implementation of {@link DataSourceResolver} that routes all realms
32+
* and store types to a
33+
* single default {@link DataSource}. This serves as both the production default
34+
* and the base for
35+
* multi-datasource extensions.
36+
*
37+
* <p>
38+
* To enable per-realm or per-store datasource routing, this class can be
39+
* extended or replaced
40+
* with a custom implementation that resolves named datasources based on
41+
* configuration.
42+
*/
43+
@ApplicationScoped
44+
public class DefaultDataSourceResolver implements DataSourceResolver {
45+
46+
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultDataSourceResolver.class);
47+
48+
private final Instance<DataSource> defaultDataSource;
49+
50+
@Inject
51+
public DefaultDataSourceResolver(Instance<DataSource> defaultDataSource) {
52+
this.defaultDataSource = defaultDataSource;
53+
}
54+
55+
@Override
56+
public DataSource resolve(String realmId, String storeType) {
57+
LOGGER.debug("Using default DataSource for realm '{}' and store '{}'", realmId, storeType);
58+
return defaultDataSource.get();
59+
}
60+
61+
@Override
62+
public Set<DataSource> getAllUniqueDataSources() {
63+
Set<DataSource> dataSources = new HashSet<>();
64+
dataSources.add(defaultDataSource.get());
65+
return dataSources;
66+
}
67+
}

persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcMetaStoreManagerFactory.java

Lines changed: 52 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@
5555
import org.slf4j.LoggerFactory;
5656

5757
/**
58-
* The implementation of Configuration interface for configuring the {@link PolarisMetaStoreManager}
58+
* The implementation of Configuration interface for configuring the
59+
* {@link PolarisMetaStoreManager}
5960
* using a JDBC backed by SQL metastore. TODO: refactor - <a
6061
* href="https://github.com/apache/polaris/pull/1287/files#r2047487588">...</a>
6162
*/
@@ -70,11 +71,15 @@ public class JdbcMetaStoreManagerFactory implements MetaStoreManagerFactory {
7071
final Map<String, Supplier<BasePersistence>> sessionSupplierMap = new HashMap<>();
7172
protected final PolarisDiagnostics diagServices = new PolarisDefaultDiagServiceImpl();
7273

73-
@Inject PolarisStorageIntegrationProvider storageIntegrationProvider;
74-
@Inject Instance<DataSource> dataSource;
75-
@Inject RelationalJdbcConfiguration relationalJdbcConfiguration;
74+
@Inject
75+
PolarisStorageIntegrationProvider storageIntegrationProvider;
76+
@Inject
77+
Instance<DataSourceResolver> dataSourceResolver;
78+
@Inject
79+
RelationalJdbcConfiguration relationalJdbcConfiguration;
7680

77-
protected JdbcMetaStoreManagerFactory() {}
81+
protected JdbcMetaStoreManagerFactory() {
82+
}
7883

7984
protected PrincipalSecretsGenerator secretsGenerator(
8085
String realmId, @Nullable RootCredentialsSet rootCredentialsSet) {
@@ -98,21 +103,24 @@ private void initializeForRealm(
98103
String realmId = realmContext.getRealmIdentifier();
99104
sessionSupplierMap.put(
100105
realmId,
101-
() ->
102-
new JdbcBasePersistenceImpl(
103-
datasourceOperations,
104-
secretsGenerator(realmId, rootCredentialsSet),
105-
storageIntegrationProvider,
106-
realmId));
106+
() -> new JdbcBasePersistenceImpl(
107+
datasourceOperations, // TODO: We need a way to pass the right data source for the actual operation
108+
// (metrics vs main),
109+
// but for now JdbcBasePersistenceImpl does everything. So we pass the main
110+
// DatasourceOperations.
111+
secretsGenerator(realmId, rootCredentialsSet),
112+
storageIntegrationProvider,
113+
realmId));
107114

108115
PolarisMetaStoreManager metaStoreManager = createNewMetaStoreManager();
109116
metaStoreManagerMap.put(realmId, metaStoreManager);
110117
}
111118

112-
public DatasourceOperations getDatasourceOperations() {
119+
public DatasourceOperations getDatasourceOperations(String realmId, String storeType) {
113120
DatasourceOperations databaseOperations;
114121
try {
115-
databaseOperations = new DatasourceOperations(dataSource.get(), relationalJdbcConfiguration);
122+
DataSource resolvedDs = dataSourceResolver.get().resolve(realmId, storeType);
123+
databaseOperations = new DatasourceOperations(resolvedDs, relationalJdbcConfiguration);
116124
} catch (SQLException sqlException) {
117125
throw new RuntimeException(sqlException);
118126
}
@@ -124,12 +132,11 @@ public synchronized Map<String, PrincipalSecretsResult> bootstrapRealms(
124132
Iterable<String> realms, RootCredentialsSet rootCredentialsSet) {
125133
SchemaOptions schemaOptions = ImmutableSchemaOptions.builder().build();
126134

127-
BootstrapOptions bootstrapOptions =
128-
ImmutableBootstrapOptions.builder()
129-
.realms(realms)
130-
.rootCredentialsSet(rootCredentialsSet)
131-
.schemaOptions(schemaOptions)
132-
.build();
135+
BootstrapOptions bootstrapOptions = ImmutableBootstrapOptions.builder()
136+
.realms(realms)
137+
.rootCredentialsSet(rootCredentialsSet)
138+
.schemaOptions(schemaOptions)
139+
.build();
133140

134141
return bootstrapRealms(bootstrapOptions);
135142
}
@@ -142,7 +149,7 @@ public synchronized Map<String, PrincipalSecretsResult> bootstrapRealms(
142149
for (String realm : bootstrapOptions.realms()) {
143150
RealmContext realmContext = () -> realm;
144151
if (!metaStoreManagerMap.containsKey(realm)) {
145-
DatasourceOperations datasourceOperations = getDatasourceOperations();
152+
DatasourceOperations datasourceOperations = getDatasourceOperations(realm, DataSourceResolver.STORE_TYPE_MAIN);
146153
try {
147154
// Run the set-up script to create the tables.
148155
datasourceOperations.executeScript(
@@ -155,8 +162,7 @@ public synchronized Map<String, PrincipalSecretsResult> bootstrapRealms(
155162
}
156163
initializeForRealm(
157164
datasourceOperations, realmContext, bootstrapOptions.rootCredentialsSet());
158-
PrincipalSecretsResult secretsResult =
159-
bootstrapServiceAndCreatePolarisPrincipalForRealm(realmContext);
165+
PrincipalSecretsResult secretsResult = bootstrapServiceAndCreatePolarisPrincipalForRealm(realmContext);
160166
results.put(realm, secretsResult);
161167
}
162168
}
@@ -188,7 +194,8 @@ public Map<String, BaseResult> purgeRealms(Iterable<String> realms) {
188194
public synchronized PolarisMetaStoreManager getOrCreateMetaStoreManager(
189195
RealmContext realmContext) {
190196
if (!metaStoreManagerMap.containsKey(realmContext.getRealmIdentifier())) {
191-
DatasourceOperations datasourceOperations = getDatasourceOperations();
197+
DatasourceOperations datasourceOperations = getDatasourceOperations(realmContext.getRealmIdentifier(),
198+
DataSourceResolver.STORE_TYPE_MAIN);
192199
initializeForRealm(datasourceOperations, realmContext, null);
193200
checkPolarisServiceBootstrappedForRealm(realmContext);
194201
}
@@ -198,7 +205,8 @@ public synchronized PolarisMetaStoreManager getOrCreateMetaStoreManager(
198205
@Override
199206
public synchronized BasePersistence getOrCreateSession(RealmContext realmContext) {
200207
if (!sessionSupplierMap.containsKey(realmContext.getRealmIdentifier())) {
201-
DatasourceOperations datasourceOperations = getDatasourceOperations();
208+
DatasourceOperations datasourceOperations = getDatasourceOperations(realmContext.getRealmIdentifier(),
209+
DataSourceResolver.STORE_TYPE_MAIN);
202210
initializeForRealm(datasourceOperations, realmContext, null);
203211
}
204212
checkPolarisServiceBootstrappedForRealm(realmContext);
@@ -219,33 +227,30 @@ public synchronized EntityCache getOrCreateEntityCache(
219227
}
220228

221229
/**
222-
* This method bootstraps service for a given realm: i.e. creates all the needed entities in the
230+
* This method bootstraps service for a given realm: i.e. creates all the needed
231+
* entities in the
223232
* metastore and creates a root service principal.
224233
*/
225234
private PrincipalSecretsResult bootstrapServiceAndCreatePolarisPrincipalForRealm(
226235
RealmContext realmContext) {
227-
// While bootstrapping we need to act as a fake privileged context since the real
236+
// While bootstrapping we need to act as a fake privileged context since the
237+
// real
228238
// CallContext may not have been resolved yet.
229-
PolarisMetaStoreManager metaStoreManager =
230-
metaStoreManagerMap.get(realmContext.getRealmIdentifier());
239+
PolarisMetaStoreManager metaStoreManager = metaStoreManagerMap.get(realmContext.getRealmIdentifier());
231240
BasePersistence metaStore = sessionSupplierMap.get(realmContext.getRealmIdentifier()).get();
232-
PolarisCallContext polarisContext =
233-
new PolarisCallContext(realmContext, metaStore, diagServices);
241+
PolarisCallContext polarisContext = new PolarisCallContext(realmContext, metaStore, diagServices);
234242

235-
Optional<PrincipalEntity> preliminaryRootPrincipal =
236-
metaStoreManager.findRootPrincipal(polarisContext);
243+
Optional<PrincipalEntity> preliminaryRootPrincipal = metaStoreManager.findRootPrincipal(polarisContext);
237244
if (preliminaryRootPrincipal.isPresent()) {
238-
String overrideMessage =
239-
"It appears this metastore manager has already been bootstrapped. "
240-
+ "To continue bootstrapping, please first purge the metastore with the `purge` command.";
245+
String overrideMessage = "It appears this metastore manager has already been bootstrapped. "
246+
+ "To continue bootstrapping, please first purge the metastore with the `purge` command.";
241247
LOGGER.error("\n\n {} \n\n", overrideMessage);
242248
throw new IllegalArgumentException(overrideMessage);
243249
}
244250

245251
metaStoreManager.bootstrapPolarisService(polarisContext);
246252

247-
PrincipalEntity rootPrincipal =
248-
metaStoreManager.findRootPrincipal(polarisContext).orElseThrow();
253+
PrincipalEntity rootPrincipal = metaStoreManager.findRootPrincipal(polarisContext).orElseThrow();
249254
return metaStoreManager.loadPrincipalSecrets(
250255
polarisContext,
251256
rootPrincipal
@@ -254,18 +259,20 @@ private PrincipalSecretsResult bootstrapServiceAndCreatePolarisPrincipalForRealm
254259
}
255260

256261
/**
257-
* In this method we check if Service was bootstrapped for a given realm, i.e. that all the
258-
* entities were created (root principal, root principal role, etc) If service was not
259-
* bootstrapped we are throwing IllegalStateException exception That will cause service to crash
260-
* and force user to run Bootstrap command and initialize MetaStore and create all the required
262+
* In this method we check if Service was bootstrapped for a given realm, i.e.
263+
* that all the
264+
* entities were created (root principal, root principal role, etc) If service
265+
* was not
266+
* bootstrapped we are throwing IllegalStateException exception That will cause
267+
* service to crash
268+
* and force user to run Bootstrap command and initialize MetaStore and create
269+
* all the required
261270
* entities
262271
*/
263272
private void checkPolarisServiceBootstrappedForRealm(RealmContext realmContext) {
264-
PolarisMetaStoreManager metaStoreManager =
265-
metaStoreManagerMap.get(realmContext.getRealmIdentifier());
273+
PolarisMetaStoreManager metaStoreManager = metaStoreManagerMap.get(realmContext.getRealmIdentifier());
266274
BasePersistence metaStore = sessionSupplierMap.get(realmContext.getRealmIdentifier()).get();
267-
PolarisCallContext polarisContext =
268-
new PolarisCallContext(realmContext, metaStore, diagServices);
275+
PolarisCallContext polarisContext = new PolarisCallContext(realmContext, metaStore, diagServices);
269276

270277
Optional<PrincipalEntity> rootPrincipal = metaStoreManager.findRootPrincipal(polarisContext);
271278
if (rootPrincipal.isEmpty()) {

0 commit comments

Comments
 (0)