Categories
Java

ThreadLocal

Bei der Migration einer größeren Anwendung (> 120.000 LOC) von einem SAP NetWeaver 7.3 mit Java 1.6 auf einen Tomcat 8.5 mit Java 1.8 hatten wir ein "interessantes" Problem:

Nach der erfolgreichen Umstellung einer Schnittstelle (SAP PI/PO) auf unseren neuen Tomcat Server wurden wir am nächsten Tag damit konfrontiert, dass die PI keinen Zugang mehr zu unserem Server hätte. Die Verbindung würde zwar aufgebaut, aber die Annahme der Daten dann mit HTTP Code 401 abgelehnt.

Das Kuriose dabei: Im Prinzip findet gar keine Authentifizierung in der Anwendung statt, denn diese Schnittstelle ist lediglich für die PI freigegeben, über einen IP Filter.

Ein Neutstart unseres Tomcat-Servers konnte das Problem kurzfristig beheben, bis es dann wieder auftrat.

Der Code des empfangenden Servlets sieht stark vereinfacht so aus:

public class IdocImportServlet extends HttpServlet implements Servlet {
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        if (UserInSession.get() == null) {
            UserInSession.put("DiesIstDiePI");
        } else {
            response.setStatus(401);
            response.getWriter().println("Illegal attempt to submit data");
            return;
        }
   		doStuff(request, response);
    }
}

Da diese Kommunikation an den üblichen Authentifizierungsmechanismen vorbei läuft, muss der UserInSession auch immer NULL sein. Aber schauen wir uns diese Klasse mal genauer an, denn der Name "InSession" ist irreführend:

public final class UserInSession {
	private static ThreadLocal sessionUser = new ThreadLocal() {
        protected synchronized Object initialValue() {
            return null;
        }
    };
	public static void put(String userId) {
        sessionUser.set(user);
    }
	public static String get() {
        return ((String) sessionUser.get());
    }
}

Unabhängig von der Session wird der User zu Begin der Verarbeitung im Servlet gesetzt. Es ist also eher ein "UserInRequest", der da gesetzt wird.
Der User wird in einer ThreadLocal gespeichert und kann so später an anderer Stelle wieder aus dieser ThreadLocale ausgelesen werden.
Das alles sollte auch kein Problem machen und so funktionieren. Und beim Debugging hat es dann auch erstmal funktioniert.

Um mir das Debugging-Leben etwas einfacher zu gestalten, habe ich die Payload einer Übertragung aus den Logfiles herausgesucht und in eine Datei (data.xml) gespeichert und dann per Terminal Befehl an die Schnittstelle zu schicken:

curl -X POST -d @data.xml http://localhost:8080/myApp/PISchnittstelle

Beim ersten Mal hat alles funktioniert und auch dann noch ein paar Mal, bis es dann nicht mehr ging.
Das Problem ließ sich beim Debuggen erkennen: der UserInSession war auf einmal schon mit dem User "DiesIstDiePI" gesetzt und daher wurde der Zugang verweigert.

Wie sich bei der Analyse herausstelle, läuft ein Request in einem Thread. So weit so gut.
Der Tomcat stellt eine bestimmte Anzahl an Threads bereit. Und nach <Anzahl der Threads +1> Requests wird der erste Thread wiederverwertet. Dummerweise ist dabei aber noch die ThreadLocale aus dem ersten Request gefüllt.

In der alten SAP NetWeaver Umgebung scheint immer ein "frischer" Thread zu kommen, so dass der Code in der alten Umgebung funktioniert hat. Wodurch genau dieses unterschiedliche Verhalten ausgelöst wird, konnten wir leider nicht in vertretbarer Zeit herausfinden.

Lösung

Die Lösung (oder vielleicht doch eher ein Workaround) besteht darin, bei jedem Request die ThreadLocal zu löschen. Gerne hätten wir nach einer Lösung gesucht, die ThreadLocal möglicherweise ganz los zu werden, aber der Aufwand wäre in Summe zu groß gewesen (was ich hier auf dieser Seite darstelle ist eine sehr stark vereinfachte und verkürzte Version des Codes und des Problems).

Um den UserInSession vor jedem Aufruf des Servlets löschen zu können, haben wir einen zusätzlichen Filter eingebaut:

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;


public class ThreadLocalFilter implements Filter {

    public void destroy() {
        // nothing to do
    }

    public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2)
            throws IOException, ServletException {
        // Wipe all ThreadLocal
        UserInSession.put(null);
        arg2.doFilter(arg0, arg1);
    }

    public void init(FilterConfig arg0) throws ServletException {
        // nothing to do
    }
}

Und diesen Filter entsprechend in der web.xml eingebaut:

    <filter>
        <filter-name>ThreadLocalFilter</filter-name>
        <filter-class>deringo.filter.ThreadLocalFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>ThreadLocalFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>