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

NullPointerException in StateNode when setting locale and using UI.getCurrent() inside localeChange callback. #11599

Closed
mrgreywater opened this issue Aug 18, 2021 · 14 comments · Fixed by #11725

Comments

@mrgreywater
Copy link

Description of the bug / feature

NullPointerException when changing locale

Minimal reproducible example

Sometimes when changing the locale when Push is enabled, Vaadin throws a NullPointerException. It's difficult to reproduce.

Expected behavior

No Exception should be thrown

Actual behavior

A NullPointer Exception is thrown because in StateNode::dumpBeforeClientResponseEntries() there is a check missing for entries == null

image

java.lang.RuntimeException: Push failed
	at com.vaadin.flow.server.communication.AtmospherePushConnection.push(AtmospherePushConnection.java:200) ~[flow-server-7.0.6.jar:7.0.6]
	at com.vaadin.flow.server.communication.AtmospherePushConnection.push(AtmospherePushConnection.java:175) ~[flow-server-7.0.6.jar:7.0.6]
	at com.vaadin.flow.component.UI.push(UI.java:686) ~[flow-server-7.0.6.jar:7.0.6]
	at com.vaadin.flow.server.VaadinSession.unlock(VaadinSession.java:708) ~[flow-server-7.0.6.jar:7.0.6]
	at com.vaadin.flow.server.VaadinService.requestEnd(VaadinService.java:1474) ~[flow-server-7.0.6.jar:7.0.6]
	at com.vaadin.flow.server.VaadinService.handleRequest(VaadinService.java:1549) ~[flow-server-7.0.6.jar:7.0.6]
	at com.vaadin.flow.server.VaadinServlet.service(VaadinServlet.java:299) ~[flow-server-7.0.6.jar:7.0.6]
	at com.vaadin.flow.spring.SpringServlet.service(SpringServlet.java:109) ~[vaadin-spring-17.0.2.jar:na]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) ~[tomcat-embed-core-9.0.45.jar:4.0.FR]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.45.jar:9.0.45]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.6.jar:5.3.6]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.6.jar:5.3.6]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.6.jar:5.3.6]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.6.jar:5.3.6]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-
	...
Caused by: java.lang.NullPointerException: Cannot invoke "java.util.ArrayList.isEmpty()" because "entries" is null
	at com.vaadin.flow.internal.StateNode.dumpBeforeClientResponseEntries(StateNode.java:1026) ~[flow-server-7.0.6.jar:7.0.6]
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195) ~[na:na]
	at java.base/java.util.HashMap$KeySpliterator.forEachRemaining(HashMap.java:1694) ~[na:na]
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484) ~[na:na]
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474) ~[na:na]
	at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913) ~[na:na]
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:na]
	at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578) ~[na:na]
	at com.vaadin.flow.internal.StateTree.flushCallbacks(StateTree.java:407) ~[flow-server-7.0.6.jar:7.0.6]
	at com.vaadin.flow.internal.StateTree.runExecutionsBeforeClientResponse(StateTree.java:384) ~[flow-server-7.0.6.jar:7.0.6]
	at com.vaadin.flow.server.communication.UidlWriter.encodeChanges(UidlWriter.java:391) ~[flow-server-7.0.6.jar:7.0.6]
	at com.vaadin.flow.server.communication.UidlWriter.createUidl(UidlWriter.java:174) ~[flow-server-7.0.6.jar:7.0.6]
	at com.vaadin.flow.server.communication.UidlWriter.createUidl(UidlWriter.java:212) ~[flow-server-7.0.6.jar:7.0.6]
	at com.vaadin.flow.server.communication.AtmospherePushConnection.push(AtmospherePushConnection.java:196) ~[flow-server-7.0.6.jar:7.0.6]
	... 46 common frames omitted

Versions:

- Vaadin / Flow version: 20.0.6
- Java version: 15
- OS version: Windows 11
- Browser version (if applicable): All
- Application Server (if applicable): Tomcat/Spring
- IDE (if applicable): IntelliJ
@mrgreywater mrgreywater changed the title Random NullPointerException when changing locale Random NullPointerException in StateNode when changing locale Aug 18, 2021
@Legioth
Copy link
Member

Legioth commented Aug 18, 2021

This is typically the symptom of doing changes without properly locking the session. How is that locale change applied?

Vaadin will apply some additional checks for potentially missing the lock if the JVM is run with assertions enabled (-ea when running from the command line). Could you try running your application with that setting to potentially get some additional information?

@mrgreywater
Copy link
Author

mrgreywater commented Aug 18, 2021

Adding the -ea parameter doesn't seem to trigger any assertion.

I get the following message as well (but only after this exception is thrown a few times):
2021-08-18 17:19:53.872 WARN 2928 --- [io-8080-exec-10] c.v.f.server.communication.PushHandler : Error while unlocking session

and

2021-08-18 17:40:40.886 ERROR 2928 --- [nio-8080-exec-7] o.a.c.c.C.[.[.[/].[app] : Servlet.service() for servlet [app] in context with path [] threw exception

Here is how the locale is changed: https://github.com/KasparScherrer/language-select/blob/master/src/main/java/ch/carnet/kasparscherrer/LanguageSelect.java#L68

It seems to only happen when the same application is open in multiple tabs. And even then you have to change locale multiple times, reload pages until at some point the exception triggers and breaks the page.

Once the exception triggers once, the tab where it triggers stays in a broken state, after that even creating new tabs trigger the exception immediately. Only when reloading with a new session (other browser, new incognito window) it's fixed.

Please also note that in the whole exception call stack, it doesn't reach my own code at any point. Vaadin just gets stuck in some kind of broken state.

@Legioth
Copy link
Member

Legioth commented Aug 18, 2021

Thanks for the additional details.

There doesn't seem to be any direct indication of missing session locking in this case since the locale change is triggered from the value change listener of a Vaadin component. There could in theory be some hidden causes e.g. if some attached component does something unconventional in their localeChange method, but that's also quite unlikely if it doesn't trigger any extra assertion with -ea.

That leaves the possibility that there's something broken with the state management related to StateNode::beforeClientResponseEntries. There is an implicit assumption that any node that is added to StateTree::pendingExecutionNodes does also have a non-null beforeClientResponseEntries field but there might be some special case when this assumption doesn't hold true. There might be something related to components being attached or detached under unsuitable circumstances or something related to the way VaadinSession::setLocale affects all UIs in the session at the same time and @Push is causing changes for all of them to be written out immediately.

@mrgreywater
Copy link
Author

mrgreywater commented Aug 18, 2021

I don't know this internal code at all, but I find it curious that both the function StateNode::hasBeforeClientResponseEntries above and the function StateNode::addBeforeClientResponseEntry below handle the case that beforeClientResponseEntries is null gracefully and don't assume it not to be null.

Btw, here are the pendingExecutionNodes in the failure state inside StateTree::flushCallbacks
image

@mshabarov
Copy link
Contributor

Can't say whether the workaround for this even exists, before making a minimal reproduce example.
I'll try to reproduce it.
Assuming that this issue comes randomly and it needs a Push to be set up, I think this is not probably about simply adding a null check into dumpBeforeClientResponseEntries().

@mshabarov
Copy link
Contributor

mshabarov commented Aug 25, 2021

I couldn't reproduce this exception.
Here are the steps I used:

  1. Example project based on Vaadin 20.0.7 with LanguageSelect component and Push:
    my-app-set-locale.zip
  2. Run mvn
  3. Open localhost:8080 in many tabs in two different browsers (chrome and safari in my case).
  4. Refresh the pages and select the language randomly on tabs, many times.

My OS is Mac, not Windows, but not sure this impacts the result anyhow.

@mrgreywater
Copy link
Author

I can't reproduce it with your test project either, but in my original project I can. I'll try to figure out out what's missing to reproduce the issue (could be the I18NProvider, a side effect of running multiple SpringApplets, or side effect of something inside the localeChange callbacks).

@mrgreywater
Copy link
Author

mrgreywater commented Aug 25, 2021

Here some notes on the changes I did to the my-app project to reproduce the bug:

Replaced the function HelloWorldView

    public HelloWorldView() {
        addClassName("hello-world-view");
        LanguageSelect languageSelect = new LanguageSelect(Locale.CANADA, Locale.CHINA, Locale.FRANCE);

        Grid<String> grid = new Grid<>();
        Grid.Column<String> column = grid.addColumn(o -> "");
        column.setHeader(new LocalizedDatePicker());

        add(languageSelect, grid);
    }

added com.example.application.components.LocalizedDatePicker

package com.example.application.components;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.datepicker.DatePicker;
import com.vaadin.flow.i18n.LocaleChangeEvent;
import com.vaadin.flow.i18n.LocaleChangeObserver;

import java.time.LocalDate;
import java.util.Locale;

public class LocalizedDatePicker extends DatePicker implements LocaleChangeObserver {
    public LocalizedDatePicker() {
    }

    private void replaceJSDateParsing(Locale locale) {
        // this check has been added after the fact
        if (getUI().orElse(null) != UI.getCurrent()) {
            System.err.println("UI mismatch!");
        }
        UI.getCurrent().beforeClientResponse(this, ctx ->
                // in the original function, actual useful code is called instead
                getElement().executeJs("console.log(this)")
        );
    }

    @Override
    public void setLocale(Locale locale) {
        super.setLocale(locale);
        this.replaceJSDateParsing(locale);
    }

    @Override
    public void localeChange(LocaleChangeEvent event) {
        setLocale(event.getLocale());
    }
}

It seems the issue stems from the fact that in the localeChange callback UI.getCurrent() returns an mismatching UI object sometimes. I'm not quite sure if this is as intended, or if the thread local UI.getCurrent() UI object should match the Component::getUI object inside the callbacks (This is what I assumed).

This was quite difficult to figure out, because the exception from Vaadin gives no clue on where the problem lies. I would love if the exceptions could be thrown earlier in cases like this with more comprehensible messages (Perhaps one could add an assertion in in UI::beforeClientResponse if this == component.getUI()?).

@mrgreywater mrgreywater changed the title Random NullPointerException in StateNode when changing locale NullPointerException in StateNode when setting locale and using UI.getCurrent() inside localeChange callback. Aug 25, 2021
@Legioth
Copy link
Member

Legioth commented Aug 26, 2021

Thanks for the detailed analysis. From this, I can make an educated guess that we have two different bugs on the radar:

  1. VaadinSession::setLocale doesn't update UI.getCurrent() when calling into each of the UIs in the session. This explains why things are happening specifically with locale changes.
  2. UI::beforeClientResponse doesn't verify that the scope component that is passed as a parameter is actually a component belonging to that particular UI. This is what allows the state to become inconsistent between StateTree and StateNode.

There might also be something related to having push enabled, even though it could also be that it only contributes to making the state corruption visible without having any issues of its own.

@mshabarov
Copy link
Contributor

Thanks, @mrgreywater , now with your codes it repeats every run.
Here is the merged example project:
my-app-set-locale-repeatable.zip.

@Legioth , concerning point 1), why do we need to update UI.getCurrent() ? Is it so that the UI instance that is stored as a weak reference (and can be cleared at any point), is not among those UI instances (uIs) in the session anymore? If so, would it be sufficient to set the current instance UI.getCurrent().setLocale(locale); in the VaadinSession::setLocale?

@Legioth
Copy link
Member

Legioth commented Aug 27, 2021

VaadinSession.setLocale is right now only doing getUIs().forEach(ui -> ui.setLocale(locale));. This means that ui.setLocale will be run with the original UI.getCurrent() value from the caller rather than the UI instance for which setLocale is invoked.

Instead, the iteration should be something along these lines:

getUIs().forEach(ui -> {
  Map<Class<?>, CurrentInstance> originalInstances = CurrentInstance.getInstances();
  try {
    UI.setCurrent(ui);
    ui.setLocale(locale)
  } finally {
    CurrentInstance.restoreInstances(originalInstances);
  }
});

vaadin-bot added a commit that referenced this issue Sep 7, 2021
vaadin-bot added a commit that referenced this issue Sep 8, 2021
denis-anisimov pushed a commit that referenced this issue Sep 9, 2021
@vaadin-bot
Copy link
Collaborator

This ticket/PR has been released with platform 21.0.1.

@vaadin-bot
Copy link
Collaborator

This ticket/PR has been released with platform 14.7.1.

@vaadin-bot
Copy link
Collaborator

This ticket/PR has been released with platform 20.0.8.

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