-
Notifications
You must be signed in to change notification settings - Fork 4.5k
Refactor database initialization logic #12961
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor database initialization logic #12961
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall looks good to me, I wonder if we can leverage lombok a little bit more to make the POJO less verbose.
|
||
while (!initialized) { | ||
getLogger().warn("Waiting for database to become available..."); | ||
if (totalTime >= getTimeoutMs()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we use a library for retrying (like https://resilience4j.readme.io/docs/retry). Instead of a custom retry policy?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@benmoriceau Definitely. However, for this pass, I am trying to simply copy/move the existing logic to the new classes to make it easier to verify like behavior. Once we get this in place, we can do follow up work to improve this logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would most likely push us to use Failsafe instead of Resilience4j, mainly because it is a bit more modern and is supported by the OpenAPI generation, should we decide to turn it on there too.
*/ | ||
default void check() throws InterruptedException { | ||
final var startTime = System.currentTimeMillis(); | ||
final var sleepTime = getTimeoutMs() / NUM_POLL_TIMES; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment about the use of the library for retrying
*/ | ||
public class ConfigsDatabaseAvailabilityCheck implements DatabaseAvailabilityCheck { | ||
|
||
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigsDatabaseAvailabilityCheck.class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit, we can use the @slf4j instead of manually instantiate the logger.
// TODO inject via dependency injection framework | ||
private final long timeoutMs; | ||
|
||
public ConfigsDatabaseAvailabilityCheck(final DSLContext dslContext, final long timeoutMs) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: we can use @AllArgConstructor to generate this constructor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@benmoriceau This constructor is most likely temporary and will be removed when we add dependency injection. At that point, we can just annotate the member fields with the injection information and can remove the constructor entirely.
return Optional.ofNullable(dslContext); | ||
} | ||
|
||
@Override |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be generated with a @Getter annotation
|
||
@Override | ||
public Optional<DSLContext> getDslContext() { | ||
return Optional.ofNullable(dslContext); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we allow a to have a null dslContext? it should be either mocked or a real context? Could we use something like https://projectlombok.org/features/NonNull to validate it in the constructor? This can be compatible with the constructor annotation as well: https://projectlombok.org/features/constructor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't. However, when we move to dependency injection, there is no guarantee that the DSLContext
bean will exist. Therefore, I am trying to be defensive here so we can raise a meaningful exception that clearly states what the issue is vs just throwing a NullPointerException
and not being obvious.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't. However, when we move to dependency injection, there is no guarantee that the DSLContext bean will exist.
This seems like it would be a recurring problem in many places, right? Does this mean we will need to wrap every bean in an optional and have explicit logic that checks if it exists and throws an error if not?
It seems like this is something that the framework should be able to enforce, i.e. bean X is required, and a meaningful exception should be thrown automatically if it does not exist at runtime. Maybe I am misunderstanding something here?
/** | ||
* Implementation of the {@link DatabaseMigrationCheck} for the Configurations database. | ||
*/ | ||
public class ConfigsDatabaseMigrationCheck implements DatabaseMigrationCheck { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have the same comment about leveraging lombok to make this class less verbose. Let me know if this would break the compatibility with micronaut.
|
||
@Test | ||
void testMigrationCheck() { | ||
final var minimumVersion = "1.0.0"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: we can use val
instead of final var
@benmoriceau I think using Lombok is a good suggestion, but I would want to wait until we convert these classes to be dependency injected. I have seen some issues with Lombok-based classes and dependency injection (too much magic being combined), so I am a bit hesitant to introduce Lombok annotations until we get to the DI work and can verify that everything works together. I should have added in the description that there will be a lot of duplication in the implementations. This is because we will need to inject different versions of the same types into the two different implementations. The only way to do that is to use qualifiers on the injection identified fields to tell the DI framework which version of a bean to inject. Once we get there, we can do another pass to consolidate as much code as possible. |
* The number of times to check if the database is available. TODO replace with a default value in a | ||
* value injection annotation | ||
*/ | ||
int NUM_POLL_TIMES = 10; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a constant, and therefore should be final
(maybe not super important given the comment above saying that this will be replaced)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a constant in an interface, so marking it final
doesn't do anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops, excuse my tunnel vision here 🤦
airbyte-db/lib/src/main/java/io/airbyte/db/check/DatabaseAvailabilityCheck.java
Outdated
Show resolved
Hide resolved
|
||
@Override | ||
public Optional<DSLContext> getDslContext() { | ||
return Optional.ofNullable(dslContext); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't. However, when we move to dependency injection, there is no guarantee that the DSLContext bean will exist.
This seems like it would be a recurring problem in many places, right? Does this mean we will need to wrap every bean in an optional and have explicit logic that checks if it exists and throws an error if not?
It seems like this is something that the framework should be able to enforce, i.e. bean X is required, and a meaningful exception should be thrown automatically if it does not exist at runtime. Maybe I am misunderstanding something here?
if (availabilityCheck.isPresent()) { | ||
availabilityCheck.get().check(); | ||
final Optional<DSLContext> dslContext = getDslContext(); | ||
if (dslContext.isPresent()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to my above comment - it feels fairly messy to have all of these isPresent()
checks, requiring us to nest the actual logic of the method multiple levels deep. Is there a path forward for removing the need for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is mainly a temporary fix, as we don't control the creation of these objects and they can therefore be null
. Once we convert to a dependency injection framework, the application will fail to start if a bean of the requested type/name cannot be found (unless of course we mark an injection point as optional).
The alternative in the current code is to put a bunch of != null
checks, which are just as messy and not the current, preferred way in Java to handle the potential for an object to not be present/set. If we didn't care about the exceptions being raised, we would use .ifPresent(o -> ...)
to remove the if checks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, I think I had misunderstood this comment as saying that we would need to keep these checks even after we fully convert to dependency injection. I'm okay with this as a temporary fix
default Collection<String> getTableNames() { | ||
return List.of(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it necessary to have this default implementation? It seems like this could lead to issues if someone implements this class without overriding this method, causing the initialize logic to basically be a no-op
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is more temporary code to avoid the possibility that the implementation doesn't set the list or override it. The alternative would be to return an Optional<Collection<String>>
to force the implementation to return an empty optional and obviously do the required ifPresent checks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might still be misunderstanding something here, but it seems to me that we would expect everything implementing this interface to have some set of tables to use for checking availability.
And if we leave this method unimplemented in the interface, isn't every implementing class required to implement it, or else Java throws a compilation error? It seems like that would enforce this
airbyte-db/lib/src/main/java/io/airbyte/db/check/DatabaseAvailabilityCheck.java
Outdated
Show resolved
Hide resolved
airbyte-db/lib/src/test/java/io/airbyte/db/check/impl/ConfigsDatabaseAvailabilityCheckTest.java
Outdated
Show resolved
Hide resolved
airbyte-db/lib/src/test/java/io/airbyte/db/check/impl/ConfigsDatabaseAvailabilityCheckTest.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still one open question about the default implementation, but I don't think it is blocking.
default Collection<String> getTableNames() { | ||
return List.of(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might still be misunderstanding something here, but it seems to me that we would expect everything implementing this interface to have some set of tables to use for checking availability.
And if we leave this method unimplemented in the interface, isn't every implementing class required to implement it, or else Java throws a compilation error? It seems like that would enforce this
@lmossman You are correct. The trade-off here is forcing the subclasses to implement it combined with null/optional checking VS having a default implementation. As you pointed out correctly, that does shadow the fact that the subclass may forget to implement it. I will convert this to use |
Might be impacted by Micronaut migration. To be revisited in the future. |
What
How
hasTable
check from the code that verifies the database is available and the migration has been performed. The logic behind this decision is that if the migration has been completed successfully, then the tables that the current code verifies would be present, making this check redundant.MinimumFlywayMigrationVersionCheck
andDatabase
classes/methods as deprecated and to be removed in the future once the conversion to the new implementation has been completed.More Details
To explain what is changing/moving to the new approach, here are some diagrams of the current code and the proposed code:
Existing Database Availability/Migration/Initialization Logic
Proposed Database Availability/Migration/Initialization Logic
One of the key differences, besides being more dependency-injection friendly, is that the proposed change in this PR re-uses the availability check logic, instead of duplicating it. In the current code, the check to verify that the database is available and accepting connections is currently duplicated between the
MinimumFlywayMigrationVersionCheck
andDatabase
classes.Recommended reading order
DatabaseCheck.java
DatabaseAvailabilityCheck.java
DatabaseMigrationCheck.java
DatabaseInitializer.java
🚨 User Impact 🚨
There is no user impact as a result of this PR, as the code being added is not currently being utilized. That will occur in a future PR.
Tests
Unit
Integration
airbyte-bootloader
andairbyte-server
and tested locally to validate that the services still performed the required checks and could start successful/perform their operations successfully when using the new logic.