Skip to content
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

VaadinSessionScopes for all sessions are destroyed when any single session expires #20092

Closed
archiecobbs opened this issue Sep 28, 2024 · 12 comments · Fixed by #20103
Closed

Comments

@archiecobbs
Copy link
Contributor

archiecobbs commented Sep 28, 2024

Description of the bug

After upgrading to Vaadin 24, I noticed a strange new warning:

2024-09-27 19:15:33,106 [Catalina-utility-1] WARN : Invocation of destroy method failed on bean with name 'myBean': java.lang.IllegalStateException: Can't access session while another session is locked by the same thread. This restriction is intended to help avoid deadlocks.

The bean MyBean is a bean annotated with @Scope(VaadinSessionScope.VAADIN_SESSION_SCOPE_NAME), i.e., it is a session-scoped bean.

I added some debugging and this is the stack trace when that error is emitted:

2024-09-27 19:15:33,104 [Catalina-utility-1] WARN : MyBean: stop() invoked with session com.vaadin.flow.server.VaadinSession@29bab2d4 != com.vaadin.flow.server.VaadinSession@fd8ac40 locked
java.lang.Throwable: HERE
	at com.example.MyBean.stop(MyBean.java:40) ~[classes/:?]
	at org.springframework.beans.factory.support.DisposableBeanAdapter.destroy(DisposableBeanAdapter.java:211) ~[spring-beans-6.1.12.jar:6.1.12]
	at org.springframework.beans.factory.support.DisposableBeanAdapter.run(DisposableBeanAdapter.java:195) ~[spring-beans-6.1.12.jar:6.1.12]
	at com.vaadin.flow.spring.scopes.BeanStore.doDestroy(BeanStore.java:110) ~[vaadin-spring-24.4.8.jar:?]
	at com.vaadin.flow.spring.scopes.VaadinSessionScope$SessionBeanStore.doDestroy(VaadinSessionScope.java:61) ~[vaadin-spring-24.4.8.jar:?]
	at com.vaadin.flow.spring.scopes.BeanStore.execute(BeanStore.java:154) ~[vaadin-spring-24.4.8.jar:?]
	at com.vaadin.flow.spring.scopes.BeanStore.destroy(BeanStore.java:98) ~[vaadin-spring-24.4.8.jar:?]
	at com.vaadin.flow.spring.scopes.VaadinSessionScope$SessionBeanStore.lambda$new$a4a1fe4f$2(VaadinSessionScope.java:53) ~[vaadin-spring-24.4.8.jar:?]
	at com.vaadin.flow.server.VaadinService.lambda$fireSessionDestroy$9c853e43$1(VaadinService.java:657) ~[flow-server-24.4.8.jar:24.4.8]
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) [?:?]
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) [?:?]
	at com.vaadin.flow.server.VaadinService.runPendingAccessTasks(VaadinService.java:2092) [flow-server-24.4.8.jar:24.4.8]
	at com.vaadin.flow.server.VaadinSession.unlock(VaadinSession.java:725) [flow-server-24.4.8.jar:24.4.8]
	at com.vaadin.flow.server.VaadinService.ensureAccessQueuePurged(VaadinService.java:2057) [flow-server-24.4.8.jar:24.4.8]
	at com.vaadin.flow.server.VaadinService.accessSession(VaadinService.java:2024) [flow-server-24.4.8.jar:24.4.8]
	at com.vaadin.flow.server.VaadinSession.access(VaadinSession.java:1029) [flow-server-24.4.8.jar:24.4.8]
	at com.vaadin.flow.server.VaadinService.fireSessionDestroy(VaadinService.java:631) [flow-server-24.4.8.jar:24.4.8]
	at com.vaadin.flow.server.VaadinSession.valueUnbound(VaadinSession.java:208) [flow-server-24.4.8.jar:24.4.8]
	at org.apache.catalina.session.StandardSession.removeAttributeInternal(StandardSession.java:1432) [catalina.jar:10.1.25]
	at org.apache.catalina.session.StandardSession.expire(StandardSession.java:679) [catalina.jar:10.1.25]
	at org.apache.catalina.session.StandardSession.isValid(StandardSession.java:508) [catalina.jar:10.1.25]
	at org.apache.catalina.session.ManagerBase.processExpires(ManagerBase.java:604) [catalina.jar:10.1.25]
	at org.apache.catalina.session.ManagerBase.backgroundProcess(ManagerBase.java:587) [catalina.jar:10.1.25]
	at org.apache.catalina.core.StandardContext.backgroundProcess(StandardContext.java:4787) [catalina.jar:10.1.25]
        ... etc ...

Basically what is happening is that session A is being shutdown, but this is causing the Spring scope associated with session B to be shutdown.

This appears to be the result of some incorrect logic in VaadinSessionScope:

public class VaadinSessionScope extends AbstractScope {
                
    public static final String VAADIN_SESSION_SCOPE_NAME = "vaadin-session";
                            
    private static class SessionBeanStore extends BeanStore {
            
        private final Registration sessionDestroyListenerRegistration;
            
        private SessionBeanStore(VaadinSession session) {
            super(session);
            if (session instanceof SpringVaadinSession) {
                sessionDestroyListenerRegistration = null;
                ((SpringVaadinSession) session)
                        .addDestroyListener(event -> destroy());
            } else {
                sessionDestroyListenerRegistration = session.getService()
                        .addSessionDestroyListener(event -> destroy());   // WRONG!
            }   
        }   

The problem is that the method VaadinService.addSessionDestroyListener() will deliver a notification for every session that is destroyed, but VaadinSessionScope should only be concerned with its own session. So it needs to ignore notifications for other, unrelated sessions, e.g., like this:

            } else {
                sessionDestroyListenerRegistration = session.getService()
                        .addSessionDestroyListener(event -> {
                            if (event.getSession() == session)
                                destroy();
                        });
            }   
        }   

As a result of this bug, if there are two VaadinSessions and one of them expires, both of them will have their VaadinSessionScope's destroyed. One particular side effect is that beans in the still-alive session are destroy()'ed and in the process they may try to lock their VaadinSession, but the dead VaadinSession's lock is already locked, generating the warning. Of course all kinds of other disasters could happen depending on the specific application.

Expected behavior

A session's Spring scope should not be shutdown when some other session expires.

Minimal reproducible example

None currently available; the problem should be obvious.

Versions

  • Vaadin / Flow version: 24.4.12
  • Java version: 17
  • OS version: Mac OS 14.6.1
  • Browser version: Firefox 130.0.1
  • Application Server: Tomcat 10.1.28
@mcollovati
Copy link
Collaborator

Thanks for reporting the issue!

Just to add some context: are you using the vaadin-spring add-on but with a custom VaadinServlet, not extending the Vaadin SpringServlet?

@archiecobbs
Copy link
Contributor Author

archiecobbs commented Sep 28, 2024

are you using the vaadin-spring add-on but with a custom VaadinServlet, not extending the Vaadin SpringServlet?

I'm doing pretty much the same thing as in https://github.com/archiecobbs/vaadin-grid-load-fail-issue

  • Using org.springframework.web.context.ContextLoaderListener to fire up Spring
  • Using com.vaadin.flow.server.VaadinServlet as the servlet for the Vaadin portion of the application
  • Including vaadin-spring (version is 24.4.8):
        <dependency>
            <groupId>com.vaadin</groupId>
            <artifactId>vaadin-spring</artifactId>
        </dependency>

...

            
    <!-- Import vaadin-bom and vaadin-spring-bom which define all the Vaadin dependencies -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.vaadin</groupId>
                <artifactId>vaadin-bom</artifactId>
                <version>${vaadin.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.vaadin</groupId>
                <artifactId>vaadin-spring-bom</artifactId>
                <version>${vaadin.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

Thanks.

@archiecobbs
Copy link
Contributor Author

The destroy listener is added though the method in SpringVaadinSession which is tied to a specific session instance and and doesn't have any direct relationship with the similarly named method in VaadinService that you're referring to.

That's correct for the true part of the if () statement but not the false part:

            } else {
                sessionDestroyListenerRegistration = session.getService()
                        .addSessionDestroyListener(event -> destroy());
            }

In the stack trace you can see that the false part is chosen on VaadinSessionScope.java line 53.

In that bit of code, session has type VaadinSession so session.getService() has type VaadinService.

@archiecobbs
Copy link
Contributor Author

FYI, I verified that this patch fixes the bug for me:

--- vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinSessionScope.java	2024-09-28 10:11:23.962928425 -0500
+++ vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinSessionScope.java	2024-09-28 10:46:20.279730421 -0500
@@ -50,7 +50,10 @@
                         .addDestroyListener(event -> destroy());
             } else {
                 sessionDestroyListenerRegistration = session.getService()
-                        .addSessionDestroyListener(event -> destroy());
+                        .addSessionDestroyListener(event -> {
+                            if (event.getSession() == session)
+                                destroy();
+                        });
             }
         }
 

@Legioth
Copy link
Member

Legioth commented Sep 30, 2024

In that bit of code, session has type VaadinSession so session.getService() has type VaadinService.

Yeah. I realized that immediately after posting so I deleted my post as it didn't really provide any value.

FYI, I verified that this patch fixes the bug for me

Good to have it verified but note that this isn't really a proper fix due to the performance characteristics of looping through all open sessions to check that new condition whenever one session is destroyed. Additionally, the listeners are kept in a CopyOnWriteArrayList which means that you're also iterating through all of them when adding or removing a listener. This feature is designed based on the assumption of having a handful of listeners but not thousands of them.

If I remember correctly, this is the reason why the special implementation was originally added to SpringVaadinSession and then it seems like we made a mistake both in reasoning and in actual implementation with the fallback in case vanilla sessions are used.

@archiecobbs
Copy link
Contributor Author

Good to have it verified but note that this isn't really a proper fix due to the performance characteristics of looping through all open sessions to check that new condition whenever one session is destroyed.

Hang on a sec... you are conflating two distinct things:

Bug 1: It's inefficient to use VaadinService.addSessionDestroyListener() to notify when a single session is being destroyed
Bug 2: SessionBeanStore should filter out SessionDestroyEvents corresponding to other sessions.

This discussion is about Bug 2. Of course Bug 1 is also a valid concern (and there should probably be a separate issue created for it) and if Bug 1 gets fixed that will surely fix Bug 2 as a side-effect, but as of now that has yet to happen so Bug 2 is still valid.

<Bug_1_Digression>

Regarding Bug 1, I have noticed this problem in the past several times in various guises. It's always the same pattern: there is some back-end resource that is allocated per-session, and we want to guarantee that it is properly cleaned up when the session disappears for any reason. But you can't just ask the VaadinSession to notify you.

How about this? We add a method like this to VaadinSession:

    /**
     * Adds a listener that gets notified when this session is destroyed.
     * This session must be currently in state {@link VaadinSessionState#OPEN}.
     * <p>              
     * This session will be locked and its {@link UI}s will have been removed
     * when the listener is called.
     *
     * @param listener the session destroy listener
     * @return a handle that can be used for removing the listener
     * @throws IllegalStateException if this session is not currently open
     * @see VaadinService#addSessionDestroyListener
     */         
    public Registration addDestroyListener(SessionDestroyListener listener);

This could be implemented by adding a list of session-specific listeners to VaadinSession.

</Bug_1_Digression>

Back to Bug 2: Other than the fact that it may someday be made obsolete by a fix to Bug 1, do you have any problems with my patch to fix Bug 2?

@Legioth
Copy link
Member

Legioth commented Sep 30, 2024

The main problem with your suggested fix for Bug 2 is that it will lead to O(n^2) performance due to Bug 1. I would suggest that we just bite the bullet right away and add an API for listening to the death of a specific session instance and then use that to fix Bug 2. The implementation is relatively trivial and it already exists in SpringVaadinSession so it could just be moved to the parent class.

The other thing on my mind is to declare it unsupported to use Vaadin's Spring scopes without also using Vaadin's Spring Servlet. That combination is apparently not widely used since it's taken this long for anyone to report the inevitable issue with disappearing bean stores.

@archiecobbs
Copy link
Contributor Author

I would suggest that we just bite the bullet right away and add an API for listening to the death of a specific session instance and then use that to fix Bug 2.

Great! I look forward to seeing that happen.

The other thing on my mind is to declare it unsupported to use Vaadin's Spring scopes without also using Vaadin's Spring Servlet.

I understand and agree with your general negative reaction here, but I believe that you're assigning the blame in the wrong place. There is nothing in any of the Vaadin "scope" implementations that cares about the specifics of the servlet.

Instead, the design problem here is evidenced by the presence of the if/instanceof/then statement in SessionBeanStore: it should not be the case that SpringVaadinSession has a general-purpose destroy listener capability that VaadinSession does not.

In other words, I'm agreeing with your statement that this capability should be moved up from SpringVaadinSession into VaadinSession where it belongs. This will clean up the design and eliminate the if/instanceof/then code.

Would you like me to prototype a PR for this or would you like to do it?

Thanks.

@mshabarov mshabarov moved this from 🆕 Needs triage to 🔖 Normal Priority (P2) in Vaadin Flow bugs & maintenance (Vaadin 10+) Oct 1, 2024
@Legioth
Copy link
Member

Legioth commented Oct 1, 2024

Would you like me to prototype a PR for this or would you like to do it?

Please go ahead if you want! 👍

I believe that you're assigning the blame in the wrong place

I'm looking at this from a product management perspective. We can surely fix this one specific design issue but there's no telling what comes next unless we also invest into improving test coverage for this configuration. I'm not sure that would be a worthwhile investment since the configuration seems to be quite a special case that isn't widely used.

archiecobbs added a commit to archiecobbs/flow that referenced this issue Oct 1, 2024
…le session expires.

* Add new method VaadinSession.addSessionDestroyListener() for when you just want one destroy
  notificatios for a single session, instead of geting notifications for all sessions via
  VaadinService.addSessionDestroyListener().

* Refactor the Spring scopes to take advantage of the new method to fix an inefficiency.

* Mark SpringVaadinSession, which is now empty and useless, for deprepcation.

Fixes vaadin#20092.
@archiecobbs archiecobbs mentioned this issue Oct 1, 2024
9 tasks
@archiecobbs
Copy link
Contributor Author

Please go ahead if you want! 👍

Thanks - I created #20103.

I'm looking at this from a product management perspective... the configuration seems to be quite a special case that isn't widely used.

My opinion only and with all due respect: The product management problem here is that Vaadin doesn't clearly define the boundary between Vaadin and Spring. As a result, every time I upgrade to a new version of Vaadin, everything Spring-related breaks because I'm not "doing Spring" exactly the way you do (which is apparently the only way you do).

For example, Vaadin seems to assume Spring boot everywhere (see previous comments). Yet, your documentation doesn't actually declare that restriction. OK, so... what does that mean??

Moreover, the documentation is lacking with regards to how exactly Vaadin does integrate with Spring. Instead of defining how the various Vaadin components connect to Spring, and what particular functions they have, and how to enable or disable these functions, the documentation just says "copy this example Spring boot project and go from there" or "just add this starter and everything will work".

For example, my application uses web.xml and positions the Vaadin servlet as just one of multiple servlets in the container, e.g. as a peer to Spring's dispatcher servlet. it's difficult for me to figure out to do this every time I upgrade (which is required because every time the old way no longer works). Then I hear stuff like "it's really not meant to run along side other servlets"... ok, then why doesn't the documentation say that? I get random weird errors like No servlet with name 'springServlet' defined in web.xml because VaadinServletConfiguration.vaadinForwardingController() adds a ServletForwardingController for some unknown and undocumented reason. Etc.

End of rant, my apologies.

@Legioth
Copy link
Member

Legioth commented Oct 2, 2024

To paraphrase Tolstoy: All Spring Boot apps are alike; each Spring app without Spring Boot is broken in its own way. Focusing on Spring Boot helps us get 80% of the benefits for 20% of the effort.

You are absolutely right that our guidance on how to set up a Spring app without Spring Boot is lacking. The thing I'm asking myself is that if we were to improve on that, then is there even a single such configuration that would let us cover 80% of the remaining cases with 20% of the effort? In other words, could we focus on a configuration that uses both scopes and SpringServlet together but explicitly declare that we don't support other configurations even though they might still happen to work? Allowing ourselves to make that simplification increases the chances that we get anything at all done because that would be a more manageable effort compared to the uncertainties involved in setting out to support all possible configurations

@archiecobbs
Copy link
Contributor Author

It's a good question. I think the answer lies in doing the necessary legwork to decompose the problem space into orthogonal chunks.

Spring Boot is designed so that users can apply the "opinionated defaults" provided by library authors for their libraries. It's not designed for library authors to mandate "opinionated defaults" for other libraries on behalf of the user - which is what Vaadin seems to be doing (and moreover without much explanation). You are a library provider, not a user, so you shouldn't be assuming anything about how the user wants to deploy their other libraries.

The good news is that Spring is, overall, very well-designed and modular, and uses the "open/closed" design philosophy. For example, it's possible to implement a Scope very cleanly and with minimal dependencies on other parts of Spring. So Vaadin should take this hint, which would mean for example that VaadinSessionScope needs nothing more than VaadinSession.getCurrent() (which is the case after PR #20103).

When Vaadin defines a VaadinSessionScope in this way, it remains totally agnostic about whether Spring Boot, or @Configuration-based bean wiring, or web.xml, or whatever is being used in the deployment. Instead, it just remains a standalone building block that can be used by all those other things however appropriate.

VaadinSessionScope is just one example of a component that should be as orthogonal as possible. All of the stuff in vaadin-spring should be similarly othogonalized to the extent possible.

Another example is Vaadin's integration with Spring Security. I had a heck of a time trying to figure out how to add a REST API to a Vaadin/Spring application using VaadinWebSecurity, which required disabled auth and CSRF checks for the REST API URIs. In a Spring-inspired world (using the open/closed principle), VaadinWebSecurity would have overridable methods for each part of the HttpSecurity configuration. Instead, Vaadin dumps it all into a single configure() method which I have to copy & paste and then pick apart. Bleh, both annoying and brittle.

In my ideal world, here is what I'd like to see wrt. Vaadin and Spring: A list of each of these "components" all properly modularized, a description of what they do, and a list dependencies between them.

Then you could just say that "The Vaadin Spring Boot Starter automatically pulls in and configures these components A, B, and C like so... ". Any person used to dealing with Spring will be able to understand what that means.

Right now, there seems to just be a bunch of undocumented magic, which is the opposite of the design philosophy of Spring and therefore frustrating to those familiar with using it.

@tepi tepi closed this as completed in 2d42aad Oct 9, 2024
@github-project-automation github-project-automation bot moved this from 🔖 Normal Priority (P2) to ✅ Closed in Vaadin Flow bugs & maintenance (Vaadin 10+) Oct 9, 2024
vaadin-bot pushed a commit that referenced this issue Oct 9, 2024
…le session expires #20092 (#20103)

* fix: VaadinSessionScopes for all sessions are destroyed when any single session expires.

* Add new method VaadinSession.addSessionDestroyListener() for when you just want one destroy
  notificatios for a single session, instead of geting notifications for all sessions via
  VaadinService.addSessionDestroyListener().

* Refactor the Spring scopes to take advantage of the new method to fix an inefficiency.

* Mark SpringVaadinSession, which is now empty and useless, for deprecation.

Fixes #20092.

* Apply formatter.

* Restore public method SpringVaadinSession.addDestroyListener().

* Restore public method SpringVaadinSession.fireSessionDestroy().

---------

Co-authored-by: caalador <[email protected]>
Co-authored-by: Teppo Kurki <[email protected]>
vaadin-bot pushed a commit that referenced this issue Oct 9, 2024
…le session expires #20092 (#20103)

* fix: VaadinSessionScopes for all sessions are destroyed when any single session expires.

* Add new method VaadinSession.addSessionDestroyListener() for when you just want one destroy
  notificatios for a single session, instead of geting notifications for all sessions via
  VaadinService.addSessionDestroyListener().

* Refactor the Spring scopes to take advantage of the new method to fix an inefficiency.

* Mark SpringVaadinSession, which is now empty and useless, for deprecation.

Fixes #20092.

* Apply formatter.

* Restore public method SpringVaadinSession.addDestroyListener().

* Restore public method SpringVaadinSession.fireSessionDestroy().

---------

Co-authored-by: caalador <[email protected]>
Co-authored-by: Teppo Kurki <[email protected]>
mcollovati pushed a commit that referenced this issue Oct 9, 2024
…le session expires #20092 (#20103) (#20203)

* fix: VaadinSessionScopes for all sessions are destroyed when any single session expires.

* Add new method VaadinSession.addSessionDestroyListener() for when you just want one destroy
  notificatios for a single session, instead of geting notifications for all sessions via
  VaadinService.addSessionDestroyListener().

* Refactor the Spring scopes to take advantage of the new method to fix an inefficiency.

* Mark SpringVaadinSession, which is now empty and useless, for deprecation.

Fixes #20092.

* Apply formatter.

* Restore public method SpringVaadinSession.addDestroyListener().

* Restore public method SpringVaadinSession.fireSessionDestroy().

---------

Co-authored-by: Archie L. Cobbs <[email protected]>
Co-authored-by: caalador <[email protected]>
Co-authored-by: Teppo Kurki <[email protected]>
mcollovati pushed a commit that referenced this issue Oct 9, 2024
…le session expires #20092 (#20103) (#20204)

* fix: VaadinSessionScopes for all sessions are destroyed when any single session expires.

* Add new method VaadinSession.addSessionDestroyListener() for when you just want one destroy
  notificatios for a single session, instead of geting notifications for all sessions via
  VaadinService.addSessionDestroyListener().

* Refactor the Spring scopes to take advantage of the new method to fix an inefficiency.

* Mark SpringVaadinSession, which is now empty and useless, for deprecation.

Fixes #20092.

* Apply formatter.

* Restore public method SpringVaadinSession.addDestroyListener().

* Restore public method SpringVaadinSession.fireSessionDestroy().

---------

Co-authored-by: Archie L. Cobbs <[email protected]>
Co-authored-by: caalador <[email protected]>
Co-authored-by: Teppo Kurki <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Development

Successfully merging a pull request may close this issue.

4 participants