Skip to content

Core: Decompose BasePersistence into disjoint SPIs#4397

Open
huaxingao wants to merge 4 commits into
apache:mainfrom
huaxingao:refactor
Open

Core: Decompose BasePersistence into disjoint SPIs#4397
huaxingao wants to merge 4 commits into
apache:mainfrom
huaxingao:refactor

Conversation

@huaxingao
Copy link
Copy Markdown
Contributor

Refactor the persistence SPIs so BasePersistence, PolicyMappingPersistence, MetricsPersistence, and IntegrationPersistence are disjoint top-level interfaces instead of BasePersistence being an aggregator. Concrete backends (JDBC, NoSQL) multi-inherit the ones they support, and TransactionalPersistence aggregates them where atomic semantics are needed.

Changes

  • BasePersistence no longer extends PolicyMappingPersistence or MetricsPersistence.
  • TransactionalPersistence now extends all four SPIs directly.
  • JdbcBasePersistenceImpl and NonFunctionalBasePersistence implement all four SPIs.
  • MetaStoreManagerFactory: rename getOrCreateSessiongetOrCreateBasePersistence; add getOrCreate{PolicyMapping,Metrics,Integration}Persistence.
  • PolarisCallContext: rename getMetaStoregetBasePersistence; add get{PolicyMapping,Metrics,Integration}Persistence.
  • Replace ((IntegrationPersistence) ms) blind casts in AtomicOperationMetaStoreManager with callCtx.getIntegrationPersistence().

Checklist

  • 🛡️ Don't disclose security issues! (contact security@apache.org)
  • 🔗 Clearly explained why the changes are needed, or linked related issues: Fixes #
  • 🧪 Added/updated tests with good coverage, or manually tested (and explained how)
  • 💡 Added comments for complex logic
  • 🧾 Updated CHANGELOG.md (if needed)
  • 📚 Updated documentation in site/content/in-dev/unreleased (if needed)

@huaxingao
Copy link
Copy Markdown
Contributor Author

@dennishuo @dimas-b @flyingImer @singhpk234
Could you please take a look at this refactor PR? Thanks a lot!


@Override
public BasePersistence getOrCreateSession(RealmContext realmContext) {
public BasePersistence getOrCreateBasePersistence(RealmContext realmContext) {
Copy link
Copy Markdown
Contributor

@dimas-b dimas-b May 12, 2026

Choose a reason for hiding this comment

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

nit: this rename causes insignificant changes in the context of this PR, which complicates review. It would be nicer to do the renaming before or after this PR 😅

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Happy to split if you'd prefer. I kept it in this PR because Dennis asked for the rename in his mailing-list reply.

Copy link
Copy Markdown
Contributor

@dimas-b dimas-b May 13, 2026

Choose a reason for hiding this comment

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

If you do not mind, let's do the rename separately and before this PR - it is a non-controversial rename - easy to review and approve :) then this (more difficult) PR will have a smaller diff (after rebasing).

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.

However... I personally already reviewed those rename changes 😅 so from my POV they are ok to keep in this PR.

* implements {@link PolicyMappingPersistence}.
*/
default PolicyMappingPersistence getOrCreatePolicyMappingPersistence(RealmContext realmContext) {
return cast(getOrCreateBasePersistence(realmContext), PolicyMappingPersistence.class);
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.

TBH, I do not really understand the purpose behind this design. We're still forcing implementation classes to support all the same interfaces that used to be extended by BasePersistence. IMHO, this make the overall design more awkward than before.

I believe the idea is/was to inject PolicyMappingPersistence into callers independently of BasePersistence. If they happen to be implemented by the same class, it should just work. However, we should not assume that (and use casts).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You comment makes sense. I have updated the PR accordingly. PTAL

@dimas-b
Copy link
Copy Markdown
Contributor

dimas-b commented May 12, 2026

Thanks for working on this, @huaxingao ! Still, I believe this PR needs more work. It is moving in the right direction, though, from my POV 👍

PolicyMappingPersistence getOrCreatePolicyMappingPersistence(RealmContext realmContext);

/** Returns the per-realm {@link MetricsPersistence}. */
MetricsPersistence getOrCreateMetricsPersistence(RealmContext 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.

MetricsPersistence is very different from "meta store" persistence... both functionally and in java SPI parameters. I believe it makes sense to use a separate factory for it.

That said, current changes are net positive, IMHO, and are moving in the right direction.

I think we can deal with the MetricsPersistence factory in a separate PR.


@Override
public BasePersistence getOrCreateSession(RealmContext realmContext) {
public BasePersistence getOrCreateBasePersistence(RealmContext 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.

However... I personally already reviewed those rename changes 😅 so from my POV they are ok to keep in this PR.


@Override
public MetricsPersistence getOrCreateMetricsPersistence(RealmContext realmContext) {
return newNoSqlMetaStore(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 think, this should return a no-op new MetricsPersistence() {} for the sake of clarity - NoSQL persistence does not implement it anyway.

Copy link
Copy Markdown
Contributor

@dimas-b dimas-b left a comment

Choose a reason for hiding this comment

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

Thanks again for working on this, @huaxingao !

Sorry for pivoting, but I think we're making positive progress overall 😅

Let's concentrate on isolating MetricsPersistence and IdempotencyPersistence from BasePersistence for now. Old use cases may be too cumbersome and can remain "as is" as far as #4269 is concerned.

abstract class NonFunctionalBasePersistence
implements BasePersistence,
IntegrationPersistence,
MetricsPersistence,
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.

Please remove MetricsPersistence from this list - it's not really implemented.

schemaVersion);
}

private JdbcBasePersistenceImpl getOrCreateJdbcPersistence(RealmContext 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.

there's no "get" logic in this method, it always "creates". Please consider renaming to createJdbcPersistence() for clarity.

ms.deleteAllEntityPolicyMappingRecords(callCtx, entity, mappingOnTarget, mappingOnPolicy);
: policyMappingPersistence.loadAllPoliciesOnTarget(
callCtx, entity.getCatalogId(), entity.getId());
policyMappingPersistence.deleteAllEntityPolicyMappingRecords(
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 think this goes against the grain of AtomicOperationMetaStoreManager javadoc, which says the class operates on one BasePersistence instance (logically).

IMHO, even the old code is not "atomic" with respect to ms.deleteAllEntityGrantRecords (line 213) and deleteAllEntityPolicyMappingRecords (line 236), but let's not aggravate this situation by introducing different objects. The old code at least invoked both methods on the same java object instance.

Sorry for missing this aspect when I initially suggested breaking BasePersistece down into all individual sub-interfaces. This code is not super easy to comprehend and keep in mind all the time 😅

With this code in mind, I think it makes sense to isolate only MetricsPersistence in this PR (and isolate idempotency persistence in #4269).

I think we need to keep this general difficulty (atomic operations) in mind for future BasePersistece refactorings, but for now I think it makes sense to keep "old" functionality intact and only isolate "new" functionality into purpose-based interfaces (MetricsPersistence, IdempotencyPersistence, etc.)

dimas-b
dimas-b previously approved these changes May 13, 2026
Copy link
Copy Markdown
Contributor

@dimas-b dimas-b left a comment

Choose a reason for hiding this comment

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

Thanks for bearing with me, @huaxingao 😅

@github-project-automation github-project-automation Bot moved this from PRs In Progress to Ready to merge in Basic Kanban Board May 13, 2026
@huaxingao
Copy link
Copy Markdown
Contributor Author

Thanks so much for the careful review, @dimas-b! Appreciate the guidance on this PR!

Copy link
Copy Markdown
Contributor

@flyingImer flyingImer left a comment

Choose a reason for hiding this comment

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

Nice first step toward making persistence SPIs independently implementable. Two small suggestions below to smooth the path for out-of-tree integrators.

* BasePersistence} so backends that do not implement metrics persistence can simply return a
* no-op instance.
*/
MetricsPersistence getOrCreateMetricsPersistence(RealmContext 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.

Consider giving this a default implementation:

default MetricsPersistence getOrCreateMetricsPersistence(RealmContext realmContext) {
  return new MetricsPersistence() {};
}

MetricsPersistence methods are all no-op by default, so the factory should be too. Backends that don't implement metrics won't need to touch their factory class. They get silent no-op for free, and the source-breaking surface shrinks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestion! I'd prefer to keep this abstract. @dimas-b 's earlier comment on the NoSQL override asked for an explicit new MetricsPersistence() {} "for the sake of clarity", my read is we should avoid a factory-level default.

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 to avoid factory-level defaults.

Also, I suggested making a separate factory for MetricsPersistence as a follow-up. If we go this way, implementation will always be complete (no need for default methods).

}

@Override
public MetricsPersistence getOrCreateMetricsPersistence(RealmContext 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.

Nit: stateless, so a private static final singleton avoids per-call allocation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants