quarkus icon indicating copy to clipboard operation
quarkus copied to clipboard

Arc: declare a synthetic, runtime-initialized bean as eagerly initialized (if conditions met)

Open yrodiere opened this issue 1 year ago • 7 comments

Description

Some Quarkus extensions would need Arc to expose the ability to declare a synthetic, runtime-initialized bean as initialized on startup -- under some circumstances.

Briefly, here's the situation:

  • By design, Datasource beans are defined at build time, when we have incomplete information about the runtime environment.
  • Application developers may not actually use all datasources at runtime:
    • The default datasource being defined implicitly, it's possble the user didn't even want it in the first place.
    • In some cases (e.g. Keycloak) the application is distributed as a binary, thus it must define a static list of datasources (postgres, oracle, mysql, sql server, ...), and people running the binary will decide to use only one of these by activating it.
  • Application developers may forget to configure a datasource (set the JDBC URL) at runtime.

And here's the need:

  • We want startup to fail if the application contains a user bean that gets injected with an active , unconfigured (no JDBC URL) datasource.
  • We want programmatic retrieval of such bean to fail similarly -- on retrieval, not when the bean is actually used!
  • We want the failures to include actionable messages:
    • The root cause for the problem, specific to each bean: "This datasource is inactive because quarkus.datasource.active is set to false"
    • For injected beans, the list of injection points
  • We want the failure to have a specific exception type, so that programmatic retrieval can catch it and ignore it (e.g. for Agroal metrics/health, or Flyway/Liquibase: those consumers just want to ignore datasources that are not available).

Note that this is not specific to datasources: other extensions need a similar feature (Hibernate ORM) and some may need it in the future (MongoDB client, Elasticsearch client, ...).

For more information about the problem, see:

  • https://github.com/quarkusio/quarkus/issues/36666#issuecomment-2162923149
  • https://groups.google.com/g/quarkus-dev/c/enMgpOrb61o/m/cRKwiWmGAgAJ
  • https://quarkusio.zulipchat.com/#narrow/stream/187038-dev/topic/.22Activate.22.2F.22Deactivate.22.20beans.20at.20runtime

And see this mind map:

arc-initialize-on-startup

Implementation ideas

Here are the conclusions of our last conversation.

The feature below only make sense for runtime-initialized, @ApplicationScoped, synthetic bean definitions. Eager initialization probably doesn't make much sense for singletons -- which are already initialized eagerly and are not proxied -- and for other scopes and pseudo-scopes (how would you initialize a @Dependent or @RequestScoped bean eagerly?).

We could add two methods to io.quarkus.arc.deployment.SyntheticBeanBuildItem.ExtendedBeanConfigurator (names are placeholders subject to bikeshedding):

  1. activeIf(condition): tells Arc that this bean is only "active" (~initializable) if the provided condition is met. Default is always active.
  2. initializeEagerly(): tells Arc that this bean should be initialized:
    • on startup if it's active (see above) and injected in a user (non-synthetic) bean. Default is to only follow CDI semantics for initialization.
    • on first retrieval (no uninitialized proxy) if retrieved programmatically. Default is to only follow CDI semantics for initialization.

The type of condition would be, depending on implementation needs (TBD):

  • A RuntimeValue<BooleanSupplier> (returns true if the condition is met)
  • A RuntimeValue<Runnable> (checks the condition, if not met throws with meaningful, actionable message)
  • A combination of the above, e.g. RuntimeValue<Condition> where Condition is defined as:
    public interface Condition extends BooleanSupplier {
    
       // returns `true` if the condition is met
       @Override
       boolean getAsBoolean();
    
       // checks the condition, if not met throws with meaningful, actionable message
       void check() throws RuntimeException;
    
    }
    

Additionally, we will need to implement two new behaviors:

  • The "eager" initialization on bean retrieval, which will throw a meaningful, typed exception (InactiveBeanException?) if the activeIf condition is not met, or wrap any exception thrown by initialization.
  • The "eager" initialization on startup, which essentially amounts to startup code that will go through all eagerly-initialized beans, and retrieve those that are injected and active, wrapping any exception with more context (the list of injection point).

In practice, we'll probably use this for datasources by setting the activeIf condition to something like "quarkus.datasource[.name].active is unset OR set to true" -- but it could be more complicated, we'll have to check. We'll also have to adapt some code that currently retrieves the datasources through various ways.

In the future we'll want to trigger initialization on startup even if a bean is only used in other synthetic beans (e.g. a datasource in a Hibernate ORM persistence unit), but that will require more work as we'll want to ignore synthetic consumers that are themselves inactive.

yrodiere avatar Jun 26 '24 11:06 yrodiere

/cc @Ladicek (arc), @manovotn (arc), @mkouba (arc)

quarkus-bot[bot] avatar Jun 26 '24 11:06 quarkus-bot[bot]

You added a link to a Zulip discussion, please make sure the description of the issue is comprehensive and doesn't require accessing Zulip

This message is automatically generated by a bot.

quarkus-bot[bot] avatar Jun 26 '24 11:06 quarkus-bot[bot]

I think that's the gist of it @Ladicek @manofthepeace @mkouba ... please let me know if anything is missing or unclear :)

yrodiere avatar Jun 26 '24 11:06 yrodiere

That would be awsome, we are implementing an application that can work with both postgres OR elastic, and we can't decide at compile time which will be chosen at runtime... Really eager to have this availbale (that would allow us to trash a whole bunch of code !)

jtama avatar Jun 26 '24 12:06 jtama

I think that's the gist of it @Ladicek @manofthepeace @mkouba ... please let me know if anything is missing or unclear :)

I think that was meant for @manovotn :)

manofthepeace avatar Jun 26 '24 13:06 manofthepeace

I think that's the gist of it @Ladicek @manofthepeace @mkouba ... please let me know if anything is missing or unclear :)

I think that was meant for @manovotn :)

It was, autocompletion slipped. Sorry :)

yrodiere avatar Jun 26 '24 14:06 yrodiere

That would be awsome, we are implementing an application that can work with both postgres OR elastic, and we can't decide at compile time which will be chosen at runtime... Really eager to have this availbale (that would allow us to trash a whole bunch of code !)

Note this was envisioned strictly as a feature for Quarkus extensions, as applications can't define synthetic beans.

You're right that it could help applications eventually though, since this feature will make it easier to support quarkus.elasticsearch.active. I intended to do that as part of #10905.

Once that's there, I think you will essentially need to:

  1. have a postgres config profile and an elasticsearch config profile
  2. set quarkus.datasource.active and quarkus.elasticsearch.active accordingly in each profile
  3. make sure to hide the elasticsearch client or datasource behind a CDI producer, which will generate a different implementation of a custom bean of yours (a postgres impl or an elasticsearch impl) based on runtime config. Something like this, except you would use programmatic bean lookup (or your app would just fail to start).

yrodiere avatar Jun 26 '24 14:06 yrodiere

could this be used for case where today @Inject StatelessSession ss still require users to have @Entity on some bean first even though a user of statlesssession does not require any entity.

i.e. https://github.com/quarkusio/quarkus/issues/7148

maxandersen avatar Jul 29 '24 10:07 maxandersen

No, that's a completely different problem.

Here, we're making it possible for a bean to become "inactive", throwing an exception when someone tries to create an instance anyway.

Ladicek avatar Jul 29 '24 10:07 Ladicek