Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,10 @@ public class DatasourceOperations {
private final RelationalJdbcConfiguration relationalJdbcConfiguration;
private final DatabaseType databaseType;

private final Random random = new Random();
private static final Random random = new Random();

public DatasourceOperations(
DataSource datasource, RelationalJdbcConfiguration relationalJdbcConfiguration)
throws SQLException {
DataSource datasource, RelationalJdbcConfiguration relationalJdbcConfiguration) {
this.datasource = datasource;
this.relationalJdbcConfiguration = relationalJdbcConfiguration;
try (Connection connection = this.datasource.getConnection()) {
Expand All @@ -87,6 +86,8 @@ public DatasourceOperations(
this.databaseType = DatabaseType.inferFromConnection(connection, configuredType);

LOGGER.info("Detected database type: {}", databaseType);
} catch (SQLException e) {
throw new RuntimeException("Failed to initialize DatasourceOperations", e);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,22 @@
import jakarta.annotation.Nullable;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Inject;
import java.sql.SQLException;
import java.time.Clock;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.sql.DataSource;
import org.apache.polaris.core.PolarisCallContext;
import org.apache.polaris.core.PolarisDiagnostics;
import org.apache.polaris.core.config.BehaviorChangeConfiguration;
import org.apache.polaris.core.config.RealmConfig;
import org.apache.polaris.core.config.RealmConfigImpl;
import org.apache.polaris.core.config.RealmConfigurationSource;
import org.apache.polaris.core.context.RealmContext;
import org.apache.polaris.core.entity.PrincipalEntity;
import org.apache.polaris.core.persistence.AtomicOperationMetaStoreManager;
Expand Down Expand Up @@ -67,19 +71,31 @@ public class JdbcMetaStoreManagerFactory implements MetaStoreManagerFactory {

private static final Logger LOGGER = LoggerFactory.getLogger(JdbcMetaStoreManagerFactory.class);

final Map<String, PolarisMetaStoreManager> metaStoreManagerMap = new HashMap<>();
final Map<String, EntityCache> entityCacheMap = new HashMap<>();
final Map<String, Supplier<BasePersistence>> sessionSupplierMap = new HashMap<>();
// Stateful per-realm cache — InMemoryEntityCache accumulates entries across requests
private final Map<String, EntityCache> entityCacheMap = new ConcurrentHashMap<>();

// Cached per-realm schema version — loaded from DB once, stable at runtime
private final ConcurrentHashMap<String, Integer> schemaVersionCache = new ConcurrentHashMap<>();

// Tracks realms that have already passed the bootstrap verification check
// (checkPolarisServiceBootstrappedForRealm), avoiding redundant DB hits on subsequent calls.
private final Set<String> verifiedRealms = ConcurrentHashMap.newKeySet();

@Inject Clock clock;
@Inject PolarisDiagnostics diagnostics;
@Inject PolarisStorageIntegrationProvider storageIntegrationProvider;
@Inject Instance<DataSource> dataSource;
@Inject RelationalJdbcConfiguration relationalJdbcConfiguration;
@Inject RealmConfig realmConfig;
@Inject DatasourceOperations datasourceOperations;
@Inject RealmConfigurationSource realmConfigurationSource;

protected JdbcMetaStoreManagerFactory() {}

@Produces
@ApplicationScoped
static DatasourceOperations produceDatasourceOperations(
Instance<DataSource> dataSource, RelationalJdbcConfiguration relationalJdbcConfiguration) {
return new DatasourceOperations(dataSource.get(), relationalJdbcConfiguration);
}

protected PrincipalSecretsGenerator secretsGenerator(
String realmId, @Nullable RootCredentialsSet rootCredentialsSet) {
if (rootCredentialsSet != null) {
Expand All @@ -93,42 +109,24 @@ protected PolarisMetaStoreManager createNewMetaStoreManager() {
return new AtomicOperationMetaStoreManager(clock, diagnostics);
}

private void initializeForRealm(
DatasourceOperations datasourceOperations,
RealmContext realmContext,
RootCredentialsSet rootCredentialsSet) {
// Materialize realmId so that background tasks that don't have an active
// RealmContext (request-scoped bean) can still create a JdbcBasePersistenceImpl
String realmId = realmContext.getRealmIdentifier();
// determine schemaVersion once per realm
final int schemaVersion =
JdbcBasePersistenceImpl.loadSchemaVersion(
datasourceOperations,
realmConfig.getConfig(BehaviorChangeConfiguration.SCHEMA_VERSION_FALL_BACK_ON_DNE));

sessionSupplierMap.put(
/** Loads and caches the schema version for the given realm (DB hit only on first call). */
private int getOrLoadSchemaVersion(String realmId, boolean fallbackOnDne) {
return schemaVersionCache.computeIfAbsent(
realmId,
() ->
new JdbcBasePersistenceImpl(
diagnostics,
datasourceOperations,
secretsGenerator(realmId, rootCredentialsSet),
storageIntegrationProvider,
realmId,
schemaVersion));

PolarisMetaStoreManager metaStoreManager = createNewMetaStoreManager();
metaStoreManagerMap.put(realmId, metaStoreManager);
k -> JdbcBasePersistenceImpl.loadSchemaVersion(datasourceOperations, fallbackOnDne));
}

public DatasourceOperations getDatasourceOperations() {
DatasourceOperations databaseOperations;
try {
databaseOperations = new DatasourceOperations(dataSource.get(), relationalJdbcConfiguration);
} catch (SQLException sqlException) {
throw new RuntimeException(sqlException);
}
return databaseOperations;
/** Creates a new stateless {@link JdbcBasePersistenceImpl} for the given realm. */
private BasePersistence createSession(
String realmId, @Nullable RootCredentialsSet rootCredentialsSet, boolean fallbackOnDne) {
int schemaVersion = getOrLoadSchemaVersion(realmId, fallbackOnDne);
return new JdbcBasePersistenceImpl(
diagnostics,
datasourceOperations,
secretsGenerator(realmId, rootCredentialsSet),
storageIntegrationProvider,
realmId,
schemaVersion);
}

@Override
Expand All @@ -153,8 +151,7 @@ public synchronized Map<String, PrincipalSecretsResult> bootstrapRealms(

for (String realm : bootstrapOptions.realms()) {
RealmContext realmContext = () -> realm;
if (!metaStoreManagerMap.containsKey(realm)) {
DatasourceOperations datasourceOperations = getDatasourceOperations();
if (!verifiedRealms.contains(realm)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check doesn’t make much sense here. The bootstrap is invoked by a CLI tool, so relying on in-memory state isn’t meaningful. Instead, we should make the process idempotent and avoid re-bootstrapping an already initialized realm. That logic should be based on the actual DB state, for example whether the root principal already exists, rather than an in-memory cache that only lives for the duration of the process.

Since the CLI tool is short-lived anyway, I’d suggest reusing checkPolarisServiceBootstrappedForRealm() here. We could decide whether split it to two methods to strip out verifiedRealms.add(realmId); but that's a minor point.

int currentSchemaVersion =
JdbcBasePersistenceImpl.loadSchemaVersion(datasourceOperations, true);
int requestedSchemaVersion = JdbcBootstrapUtils.getRequestedSchemaVersion(bootstrapOptions);
Expand All @@ -178,75 +175,91 @@ public synchronized Map<String, PrincipalSecretsResult> bootstrapRealms(
throw new RuntimeException(
String.format("Error executing sql script: %s", e.getMessage()), e);
}
initializeForRealm(
datasourceOperations, realmContext, bootstrapOptions.rootCredentialsSet());
// Cache the effective schema version for this realm
schemaVersionCache.put(realm, effectiveSchemaVersion);
Comment thread
singhpk234 marked this conversation as resolved.

PolarisMetaStoreManager metaStoreManager =
metaStoreManagerMap.get(realmContext.getRealmIdentifier());
BasePersistence metaStore = sessionSupplierMap.get(realmContext.getRealmIdentifier()).get();
PolarisMetaStoreManager metaStoreManager = createNewMetaStoreManager();
BasePersistence metaStore =
createSession(realm, bootstrapOptions.rootCredentialsSet(), true);
PolarisCallContext polarisContext = new PolarisCallContext(realmContext, metaStore);

PrincipalSecretsResult secretsResult =
createPolarisPrincipalForRealm(metaStoreManager, polarisContext);
results.put(realm, secretsResult);
verifiedRealms.add(realm);
}
}

return Map.copyOf(results);
}

@Override
public Map<String, BaseResult> purgeRealms(Iterable<String> realms) {
public synchronized Map<String, BaseResult> purgeRealms(Iterable<String> realms) {
Map<String, BaseResult> results = new HashMap<>();

for (String realm : realms) {
RealmContext realmContext = () -> realm;
PolarisMetaStoreManager metaStoreManager = getOrCreateMetaStoreManager(realmContext);
BasePersistence session = getOrCreateSession(realmContext);
PolarisMetaStoreManager metaStoreManager = createNewMetaStoreManager();
BasePersistence session = createSession(realm, null, true);

PolarisCallContext callContext = new PolarisCallContext(realmContext, session);

// Verify the realm is bootstrapped before purging — a non-bootstrapped realm
// has no root principal, so purging it is a no-op that should be reported as failure.
Optional<PrincipalEntity> rootPrincipal = metaStoreManager.findRootPrincipal(callContext);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, I think this is the right thing to do.

if (rootPrincipal.isEmpty()) {
results.put(
realm, new BaseResult(BaseResult.ReturnStatus.ENTITY_NOT_FOUND, "Not bootstrapped"));
continue;
}

BaseResult result = metaStoreManager.purge(callContext);
results.put(realm, result);

sessionSupplierMap.remove(realm);
metaStoreManagerMap.remove(realm);
// Evict all cached state for this realm
entityCacheMap.remove(realm);
schemaVersionCache.remove(realm);
verifiedRealms.remove(realm);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

}

return Map.copyOf(results);
}

@Override
public synchronized PolarisMetaStoreManager getOrCreateMetaStoreManager(
RealmContext realmContext) {
if (!metaStoreManagerMap.containsKey(realmContext.getRealmIdentifier())) {
DatasourceOperations datasourceOperations = getDatasourceOperations();
initializeForRealm(datasourceOperations, realmContext, null);
checkPolarisServiceBootstrappedForRealm(realmContext);
}
return metaStoreManagerMap.get(realmContext.getRealmIdentifier());
public PolarisMetaStoreManager getOrCreateMetaStoreManager(RealmContext realmContext) {
// Stateless — create a fresh instance on every call, no caching needed
return createNewMetaStoreManager();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: why not use the one in the entity cache? The caller is request scoped,

public PolarisMetaStoreManager polarisMetaStoreManager(
, I guess the point of caching is to reuse it here. Am I missing something?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AtomicOperationMetaStoreManager is a light-weight java object. It's probably not worth reusing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we put it in the entity cache then?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question 😉 ... but I think it'll go way outside the scope of this PR.

}

@Override
public synchronized BasePersistence getOrCreateSession(RealmContext realmContext) {
if (!sessionSupplierMap.containsKey(realmContext.getRealmIdentifier())) {
DatasourceOperations datasourceOperations = getDatasourceOperations();
initializeForRealm(datasourceOperations, realmContext, null);
public BasePersistence getOrCreateSession(RealmContext realmContext) {
String realmId = realmContext.getRealmIdentifier();
RealmConfig realmConfig = new RealmConfigImpl(realmConfigurationSource, realmContext);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose this is related to the following comment from the main thread:

[...] It calls metaStoreManagerFactory.getOrCreateSession(realmContext), which in your refactored code reads from realmConfig - a @RequestScoped CDI bean. That throws ContextNotActiveException

This means the realmConfig was from a different request scope. In other words, something was reused across different requests without proper context propagation.

IMHO, this is not a problem in this factory, but in the caller.

We need to review the caller and handle context propagation / activation properly. Cf. #4061

@adutra : WDYT?

boolean fallbackOnDne =
realmConfig.getConfig(BehaviorChangeConfiguration.SCHEMA_VERSION_FALL_BACK_ON_DNE);

// Verify bootstrap once per realm lifetime; skip on subsequent calls.
// On cold start, multiple threads may verify concurrently — this is benign
// (idempotent DB query), trading a few redundant queries for simpler code.
if (!verifiedRealms.contains(realmId)) {
checkPolarisServiceBootstrappedForRealm(realmContext, fallbackOnDne);
}
checkPolarisServiceBootstrappedForRealm(realmContext);
return sessionSupplierMap.get(realmContext.getRealmIdentifier()).get();

// Stateless — create a fresh instance on every call; schemaVersion is cached per realm
return createSession(realmId, null, fallbackOnDne);
}

@Override
public synchronized EntityCache getOrCreateEntityCache(
RealmContext realmContext, RealmConfig realmConfig) {
if (!entityCacheMap.containsKey(realmContext.getRealmIdentifier())) {
PolarisMetaStoreManager metaStoreManager = getOrCreateMetaStoreManager(realmContext);
entityCacheMap.put(
realmContext.getRealmIdentifier(),
new InMemoryEntityCache(diagnostics, realmConfig, metaStoreManager));
}

return entityCacheMap.get(realmContext.getRealmIdentifier());
public EntityCache getOrCreateEntityCache(RealmContext realmContext, RealmConfig realmConfig) {
String realmId = realmContext.getRealmIdentifier();
// EntityCache is stateful (Caffeine + ConcurrentHashMap) — must be shared across requests.
// ConcurrentHashMap.computeIfAbsent is already atomic — no external lock needed.
return entityCacheMap.computeIfAbsent(
realmId,
k -> {
PolarisMetaStoreManager metaStoreManager = createNewMetaStoreManager();
return new InMemoryEntityCache(diagnostics, realmConfig, metaStoreManager);
});
}

/**
Expand All @@ -256,19 +269,21 @@ public synchronized EntityCache getOrCreateEntityCache(
* and force user to run Bootstrap command and initialize MetaStore and create all the required
* entities
*/
private void checkPolarisServiceBootstrappedForRealm(RealmContext realmContext) {
PolarisMetaStoreManager metaStoreManager =
metaStoreManagerMap.get(realmContext.getRealmIdentifier());
BasePersistence metaStore = sessionSupplierMap.get(realmContext.getRealmIdentifier()).get();
private void checkPolarisServiceBootstrappedForRealm(
RealmContext realmContext, boolean fallbackOnDne) {
String realmId = realmContext.getRealmIdentifier();
PolarisMetaStoreManager metaStoreManager = createNewMetaStoreManager();
BasePersistence metaStore = createSession(realmId, null, fallbackOnDne);
PolarisCallContext polarisContext = new PolarisCallContext(realmContext, metaStore);

Optional<PrincipalEntity> rootPrincipal = metaStoreManager.findRootPrincipal(polarisContext);
if (rootPrincipal.isEmpty()) {
LOGGER.error(
"\n\n Realm {} is not bootstrapped, could not load root principal. Please run Bootstrap command. \n\n",
realmContext.getRealmIdentifier());
realmId);
throw new IllegalStateException(
"Realm is not bootstrapped, please run server in bootstrap mode.");
}
verifiedRealms.add(realmId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.Produces;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.apache.polaris.core.config.ProductionReadinessCheck;
import org.apache.polaris.core.persistence.MetaStoreManagerFactory;

Expand All @@ -32,26 +30,17 @@ public class RelationalJdbcProductionReadinessChecks {
@Produces
public ProductionReadinessCheck checkRelationalJdbc(
MetaStoreManagerFactory metaStoreManagerFactory,
Instance<DataSource> dataSource,
RelationalJdbcConfiguration relationalJdbcConfiguration) {
Instance<DatasourceOperations> datasourceOperations) {
// This check should only be applicable when persistence uses RelationalJdbc.
if (!(metaStoreManagerFactory instanceof JdbcMetaStoreManagerFactory)) {
return ProductionReadinessCheck.OK;
}

try {
DatasourceOperations datasourceOperations =
new DatasourceOperations(dataSource.get(), relationalJdbcConfiguration);
if (datasourceOperations.getDatabaseType().equals(DatabaseType.H2)) {
return ProductionReadinessCheck.of(
ProductionReadinessCheck.Error.of(
"The current persistence (jdbc:h2) is intended for tests only.",
"quarkus.datasource.jdbc.url"));
}
} catch (SQLException e) {
if (datasourceOperations.get().getDatabaseType().equals(DatabaseType.H2)) {
return ProductionReadinessCheck.of(
ProductionReadinessCheck.Error.of(
"Misconfigured JDBC datasource", "quarkus.datasource.jdbc.url"));
"The current persistence (jdbc:h2) is intended for tests only.",
"quarkus.datasource.jdbc.url"));
}
return ProductionReadinessCheck.OK;
}
Expand Down
Loading