Skip to content
This repository was archived by the owner on Jan 9, 2025. It is now read-only.

Set appropriate ClassLoaderProvider in Spring Boot integrations#518

Merged
fh-ms merged 6 commits intomicrostream-one:masterfrom
magneticflux-:spring-classloader-fix
Apr 21, 2023
Merged

Set appropriate ClassLoaderProvider in Spring Boot integrations#518
fh-ms merged 6 commits intomicrostream-one:masterfrom
magneticflux-:spring-classloader-fix

Conversation

@magneticflux-
Copy link
Copy Markdown
Contributor

@magneticflux- magneticflux- commented Jan 29, 2023

Closes #517

I wasn't able to add failing integration tests because Spring Boot DevTools checks if it's running in a test environment or in GraalVM multiple times, and deactivates itself.

@magneticflux-
Copy link
Copy Markdown
Contributor Author

magneticflux- commented Jan 29, 2023

ec2608d is the important commit. I can revert a976b30 if it's not wanted; I was just cleaning up the areas I touched with the other commit since they were still using raw types everywhere.

@magneticflux-
Copy link
Copy Markdown
Contributor Author

This also might mean one.microstream.use-current-thread-class-loader workaround can be removed, since all classes are loaded using the "main" ApplicationContext class loader. I can continue this PR to remove that workaround if you want, I don't think it would take very long.

ClassLoader classLoader = applicationContext.getClassLoader();
// Fall back to the current ClassLoader if null
if (classLoader == null)
classLoader = this.getClass().getClassLoader();
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.

https://docs.spring.io/spring-boot/docs/1.5.16.RELEASE/reference/html/using-boot-devtools.html
20.2.6 Known limitations
Question: Wouldn't it be better to use: Thread.currentThread().getContextClassLoader() ?

Copy link
Copy Markdown
Contributor Author

@magneticflux- magneticflux- Feb 1, 2023

Choose a reason for hiding this comment

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

(I might have misunderstood your comment, this reply assumed you meant replacing the entire ClassLoader search with Thread.currentThread().getContextClassLoader() alone and ignoring the ApplicationContext)


I'm a bit wary of forcing users to use the current thread's context ClassLoader, rather than using the application-context-supplied one. ResourceLoader#getClassLoader (supertype of ApplicationContext) states:

Clients which need to access the ClassLoader directly can do so in a uniform manner with the ResourceLoader, rather than relying on the thread context ClassLoader.

which I think is a more reliable technique in case a user wants to configure some Spring property or callback that changes the application context ClassLoader.


Additional context:

This answer on StackOverflow is quite vocal about the use of Thread.currentThread().getContextClassLoader() https://stackoverflow.com/a/36228195/9073728:

If a library uses hacks such as Thread.getContextClassLoader(), sun.misc.VM.latestUserDefinedLoader(), or sun.reflect.Reflection.getCallerClass() it is a bug caused by a deficiency in the API. Basically, Thread.getContextClassLoader() exists only because whoever designed the ObjectInputStream API forgot to accept the ClassLoader as a parameter, and this mistake has haunted the Java community to this day.

By default, getting the application context's ClassLoader delegates from AnnotationConfigServletWebServerApplicationContext to ClassLoaderFilesResourcePatternResolver to ServletContextResourcePatternResolver to WebApplicationContextResourceLoader to DefaultResourceLoader which finally checks its provided classLoader (null by default) and falls back to ClassUtils.getDefaultClassLoader() which calls Thread.currentThread().getContextClassLoader() first, so by default the two sources of context ClassLoaders should be the same.

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.

I actually think the fallback can be removed entirely and replaced with an exception, since ResourceLoader#getClassLoader's documentation also states:

Returns: the ClassLoader (only null if even the system ClassLoader isn't accessible)

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.

Okay, my comment was only about the fallback.
I generally like your solution, I'm just trying to see if it might have some other implications that I haven't thought of at the moment.

@rdebusscher
Copy link
Copy Markdown

Hi @magneticflux-

This PR doesn't solve the issue with the DevTools completely in my test application. With the changes, the application starts (and does not complain about the missing bean) but when making a change in the application and with the restart, I still get the Class cast exception because of the different classloaders that are in use.

The solution is to put this

restart.include.microstream=/microstream-.*.jar

within spring-devtools.properties file, just as you need to do with version 7.1 of MicroStream.

What am I missing?

@magneticflux-
Copy link
Copy Markdown
Contributor Author

magneticflux- commented Feb 2, 2023

@rdebusscher I am able to reproduce that issue, and it occurs because the StorageManagerProvider retains a static map that caches already-created storage managers. We don't want to recreate the EmbeddedStorageManager each reload though, so instead of injecting the current ApplicationContext and retaining it, we can inject a ObjectFactory<ApplicationContext> that queries the current context whenever a ClassLoader is needed. Does 5f31c0d fix the issue you were seeing?

@magneticflux-
Copy link
Copy Markdown
Contributor Author

magneticflux- commented Feb 2, 2023

I also ran into another issue, unrelated to Spring Boot. When redefining classes using DCEVM (only Java 8, use TravaOpenJDK for Java 11, or JetBrainsRuntime for Java 11 or 17), a BinaryStorer.Item with an object and corresponding PersistenceTypeHandler uses out-of-date memoryOffsets and storers. This can cause data corruption or crashes in development after adjusting fields in a live object, but doesn't cause issues if you aren't storing objects you redefine.

This doesn't seem easily fixable though. If you want to keep caching reflective accesses (like BinaryHandlerGenericType does with persistableFields), you'd need to check the private field classRedefinedCount on each Class and recompute if it changes.

Incremented by the VM on each call to JVM TI RedefineClasses() that redefines this class or a superclass.

private transient volatile int classRedefinedCount;

@rdebusscher
Copy link
Copy Markdown

Hi @magneticflux-

The usage of ObjectFactory<ApplicationContext> doesn't solve it since the classloader is not the main problem. As you mentioned, some static map holds the old classes and causes the ClassCastException.

And the usage of the spring-devtools.properties file solves this by making sure that the MicroStream classes are part of the restart classloader. But the usage of the restart classloader is not causing the recreation of the EmbeddedStorageManager. Reload in Spring Boot drops the Spring Bean container and recreates it. The MicroStream Spring Boot integration defines the EmbeddedStorageManager as bean and thus recreation of the Storage Manager is part of the expected behaviour of Reload functionality.

@magneticflux-
Copy link
Copy Markdown
Contributor Author

I disagree with adding MicroStream's classes to the restart classloader. The restart classloader is only supposed to contain classes that are possible to change - i.e. the end-user's classes. MicroStream internal classes do not change, and thus should not be included in the restart classloader.

As it stands now, in the current release of MicroStream, reloading the Spring application does destroy the EmbeddedStorageManager by calling the shutdown() method. However, reloading the application does not reload the StorageManagerProvider class, so its static field is not cleared and the old, shutdown, EmbeddedStorageManagers are able to be retrieved by StorageManagerFactory after the restart.

Since StorageManagerProvider is already a singleton (by default), I suggest we make the static map field an instance field. This would allow it to forget the EmbeddedStorageManager beans and create new ones after restarting.

Additionally, I think I have a workaround for my class redefinition issue using Spring's LoadTimeWeaverAware. We might be able to define a no-op ClassFileTransformer that watches for classes persisted by MicroStream and throws UnsupportedOperationException if the class is being modified. IntelliJ's default behavior when redefinition fails is to trigger a restart, which would then correctly load and persist the new classes.


I will fixup+rebase my initial commit to make StorageManagerProvider's static field an instance field, and add a new commit attempting to prevent stored classes from being redefined.

The component is already a singleton, so having a static field to store `EmbeddedStorageManager`s just leaks them between restarts. They already get shutdown on restart since they are Beans, so the leaked `EmbeddedStorageManager`s aren't usable.
@magneticflux- magneticflux- force-pushed the spring-classloader-fix branch from 5f31c0d to 1e8433b Compare February 3, 2023 07:27
@magneticflux-
Copy link
Copy Markdown
Contributor Author

My technique to prevent storage classes from being redefined did not work because Java Agent self-Instrumentation is not supported, and that's the only way to receive notification of class redefinition events. If you want resilience against class redefinition, I think the only way is to check the classRedefinedCount field for changes upon (de)serialization.

@rdebusscher
Copy link
Copy Markdown

From within IDE, the Spring Boot app is working, with reload. But when running through 'java -jar', I get an null Root (when dev-tools as optional as in documentation, without this dependency, it also works). So I need more time to test out what is going on.

The Spring application context's class loader is now always used.
@magneticflux-
Copy link
Copy Markdown
Contributor Author

@rdebusscher I've tried to reproduce the issue you mentioned while running the Spring Boot jar directly, but I couldn't using the Gradle starter project. I've shared my reproduction here: https://github.com/magneticflux-/microstream-issue517-demo, it requires the snapshot to be installed to your local Maven repo.

Using that reproduction:

  • I can successfully run...
    • Inside IntelliJ IDEA
    • Using Gradle ./gradlew bootRun
    • Directly using ./gradlew bootJar && java -jar build/libs/microstream-issue517-demo-0.0.1-SNAPSHOT.jar

Details:

When running inside IntelliJ, changing application.properties, clicking "Build Project" (to copy resources), and then clicking "Update" causes the application to perform a quick restart (~0.3 seconds instead of ~5 seconds for a full restart) which properly shuts down and restarts the MicroStream storage system.

While in debug mode inside IntelliJ, changing a class and clicking "Build Project" causes the class to be reloaded near-instantly without restarting the application. Like I mentioned before, this can cause issues with serialization when the reloaded class has already had a PersistenceTypeHandler that caches the list of field types and offsets.

@rdebusscher
Copy link
Copy Markdown

I found the problem, asynchronous initialisation which means you can operate on a null Root. Not related to this PR. #530

@rdebusscher
Copy link
Copy Markdown

We have a look what is needed (regarding intellectual Property flow) to accept your PR as we are moving the code to the Eclipse Foundation.

Copy link
Copy Markdown

@rdebusscher rdebusscher left a comment

Choose a reason for hiding this comment

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

Since integrations are not donated to Eclipse Foundation, no additional paperwork is needed.

@fh-ms fh-ms added this to the 08.00.00 milestone Apr 20, 2023
@fh-ms fh-ms merged commit 0cb5944 into microstream-one:master Apr 21, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Annotated storage bean not found when using Spring Boot DevTools RestartClassLoader

4 participants