3
\$\begingroup\$

As a part of library implementing websocket Guice scopes I need to maintain an object related to each HttpSession called sessionContextAttributes. Currently sessionContextAttributes is stored as an attribute of a session, which means the access and initialization needs to be synchronized:

ConcurrentMap<Key<?>, Object> getHttpSessionContextAttributes() {
    HttpSession session = getHttpSession();
    try {
        synchronized (session) {
            @SuppressWarnings("unchecked")
            var sessionContextAttributes = (ConcurrentMap<Key<?>, Object>)
                session.getAttribute(SESSION_CONTEXT_ATTRIBUTE_NAME);
            if (sessionContextAttributes == null) {
                sessionContextAttributes = new ConcurrentHashMap<>();
                session.setAttribute(SESSION_CONTEXT_ATTRIBUTE_NAME, sessionContextAttributes);
            }
            return sessionContextAttributes;
        }
    } catch (NullPointerException e) { /* ... */ }
}

static final String SESSION_CONTEXT_ATTRIBUTE_NAME =
        ContainerCallContext.class.getPackageName() + ".contextAttributes";

Now I'm considering to maintain a static ConcurrentMap from HttpSession to sessionContextAttributes like this:

static final ConcurrentMap<HttpSession, ConcurrentMap<Key<?>, Object>> sessionCtxs =
        new ConcurrentHashMap<>();

ConcurrentMap<Key<?>, Object> getHttpSessionContextAttributes() {
    try {
        return sessionCtxs.computeIfAbsent(
                getHttpSession(), (ignored) -> new ConcurrentHashMap<>());
    } catch (NullPointerException e) { /* ... */ }
}

This simplifies getHttpSessionContextAttributes() method and is probably slightly faster, BUT introduces static data AND requires manual removal to prevent leaking of sessionContextAttributes:

static class SessionContextJanitor implements HttpSessionListener {

    @Override
    public void sessionDestroyed(HttpSessionEvent event) {
        sessionCtxs.remove(event.getSession());  // this avoids resource leaks
    }
}

I cannot confidently tell which approach is better: both have pros&cons. Hence I'm posting it here for review: maybe someone knows some reasons that make 1 of these approaches significantly better than the other.

\$\endgroup\$

1 Answer 1

0
\$\begingroup\$

After some more research and thinking, it turned out that both of the above approaches are wrong ;-]
The reason for both cases is that user Filters may put HttpSession object into a wrapper, so neither synchronizing on it nor using it as a key in a ConcurrentHashMap is safe :/
Furthermore, in case of "maintaining a static ConcurrentMap" solution, sessionContextAttributes would not get serialized together with its session for example when sharing between cluster nodes or temporarily storing on disk to use memory for other requests.

Instead, I use HttpSessionListener to create and store the new context as a session attribute:

ConcurrentMap<Key<?>, Object> getHttpSessionContextAttributes() {
    try {
        return (ConcurrentMap<Key<?>, Object>)
                getHttpSession().getAttribute(SESSION_CONTEXT_ATTRIBUTE_NAME);
    } catch (NullPointerException e) {
        throw new RuntimeException(
            "No session in call context. Consider either using a filter"
            + " that creates a session for every incoming websocket request or"
            + " using websocket connection scope instead."
        );
    }
}

static class SessionContextCreator implements HttpSessionListener {

    @Override
    public void sessionCreated(HttpSessionEvent event) {
        event.getSession().setAttribute(
            SESSION_CONTEXT_ATTRIBUTE_NAME,
            new ConcurrentHashMap<Key<?>, Object>()
        );
    }
}

While it is not explicitly stated in the spec that sessionCreated(...) will be called on the same thread that first created the session with request.getSession(), it is kinda implied and both Tomcat and Jetty do so. Last but not least, certain Spring utilities assume that this is always the case.
Further discussions about this: 1, 2, 3

\$\endgroup\$
7
  • \$\begingroup\$ Instead of } catch (NullPointerException e) { comment maybe just if (getHttpSession() != null) ...? (or some direct session check if it is getter's implementation dependend) \$\endgroup\$
    – Azahe
    Nov 12, 2021 at 10:49
  • \$\begingroup\$ @Azahe NPE there is a result of a bug in the app that uses this library: it should be eliminated during development, so there's no point to check it every time in production where it will never happen. I catch it only to throw RuntimeException with a message that explains how to fix this bug. See this article for further discussion on the topic: dzone.com/articles/why-i-never-null-check-parameters \$\endgroup\$
    – morgwai
    Nov 13, 2021 at 12:40
  • \$\begingroup\$ @Azahe in the post here the handling has been replaced with the comment not to bother ppl with reading code that is not related to the core of the question ;-) \$\endgroup\$
    – morgwai
    Nov 13, 2021 at 12:47
  • \$\begingroup\$ As much as I love the premise of article you mentioned (and consider it a standard approach at my workplace) I don't think that it any way justifies this way of handling programming errors. Note that the main idea of the article is to get rid of null checks by better design of the code - so here catching null pointer is IMO misappropriation of the idea as you are not eliminating the possibility of null pointer via redesigning library api so that making the known mistake impossible (like in the article's example with method parameters). \$\endgroup\$
    – Azahe
    Nov 16, 2021 at 10:37
  • \$\begingroup\$ @Azahe you are correct, unfortunately the only way to eliminate possibility of this error would be to force session creation for every request starting a websocket connection: it is not acceptable in many cases and I didn't want to limit applicability of this lib. Reasons may be technical (cookies disabled, non-browser clients that don't even follow redirections), legal (user explicitly refusing any data storage) and probably others. Notifying developer ASAP about the issue with a clear message, was the best I could think of without compromising applicability. i's a sad trade-off basically... \$\endgroup\$
    – morgwai
    Nov 16, 2021 at 11:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.