Hatena::ブログ(Diary)

今日のひとこと このページをアンテナに追加 RSSフィード Twitter

FessプロジェクトS2Robotプロジェクトで開発したい人を募集中

2010年02月11日

[][][]clearReferences系のエラーログの対応方法 12:07 clearReferences系のエラーログの対応方法を含むブックマーク clearReferences系のエラーログの対応方法のブックマークコメント

調査を進めた。clearReferencesThreadsとclearThreadLocalMapのエラーを黙らせるには以下のようなコードサーブレットのdestroyに書けば良い(サーブレット定義順には注意)。

   @Override
    public void destroy() {
        stopAllThreads();
    }

    private void stopAllThreads() {
        Thread[] threads = getThreads();
        ClassLoader cl = this.getClass().getClassLoader();

        List<String> jvmThreadGroupList = new ArrayList<String>();
        jvmThreadGroupList.add("system");
        jvmThreadGroupList.add("RMI Runtime");

        // Iterate over the set of threads
        for (Thread thread : threads) {
            if (thread != null) {
                ClassLoader ccl = thread.getContextClassLoader();
                if (ccl != null && ccl == cl) {
                    // Don't warn about this thread
                    if (thread == Thread.currentThread()) {
                        continue;
                    }

                    // Don't warn about JVM controlled threads
                    ThreadGroup tg = thread.getThreadGroup();
                    if (tg != null && jvmThreadGroupList.contains(tg.getName())) {
                        continue;
                    }

                    waitThread(thread);
                    // Skip threads that have already died
                    if (!thread.isAlive()) {
                        continue;
                    }

                    if (logger.isInfoEnabled()) {
                        logger.info("Interrupting a thread ["
                                + thread.getName() + "]...");
                    }
                    thread.interrupt();

                    waitThread(thread);
                    // Skip threads that have already died
                    if (!thread.isAlive()) {
                        continue;
                    }

                    if (logger.isInfoEnabled()) {
                        logger.info("Stopping a thread [" + thread.getName()
                                + "]...");
                    }
                    thread.stop();
                }
            }
        }

        Field threadLocalsField = null;
        Field inheritableThreadLocalsField = null;
        Field tableField = null;
        try {
            threadLocalsField = Thread.class.getDeclaredField("threadLocals");
            threadLocalsField.setAccessible(true);
            inheritableThreadLocalsField = Thread.class
                    .getDeclaredField("inheritableThreadLocals");
            inheritableThreadLocalsField.setAccessible(true);
            // Make the underlying array of ThreadLoad.ThreadLocalMap.Entry objects
            // accessible
            Class<?> tlmClass = Class
                    .forName("java.lang.ThreadLocal$ThreadLocalMap");
            tableField = tlmClass.getDeclaredField("table");
            tableField.setAccessible(true);
        } catch (Exception e) {
            // ignore
        }
        for (Thread thread : threads) {
            if (thread != null) {

                Object threadLocalMap;
                try {
                    // Clear the first map
                    threadLocalMap = threadLocalsField.get(thread);
                    clearThreadLocalMap(cl, threadLocalMap, tableField);
                } catch (Exception e) {
                    // ignore
                }
                try { // Clear the second map
                    threadLocalMap = inheritableThreadLocalsField.get(thread);
                    clearThreadLocalMap(cl, threadLocalMap, tableField);
                } catch (Exception e) {
                    // ignore
                }
            }
        }
    }

    private void waitThread(Thread thread) {
        int count = 0;
        while (thread.isAlive() && count < 5) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
            }
            count++;
        }
    }

    /*
     * Get the set of current threads as an array.
     */
    private Thread[] getThreads() {
        // Get the current thread group
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        // Find the root thread group
        while (tg.getParent() != null) {
            tg = tg.getParent();
        }

        int threadCountGuess = tg.activeCount() + 50;
        Thread[] threads = new Thread[threadCountGuess];
        int threadCountActual = tg.enumerate(threads);
        // Make sure we don't miss any threads
        while (threadCountActual == threadCountGuess) {
            threadCountGuess *= 2;
            threads = new Thread[threadCountGuess];
            // Note tg.enumerate(Thread[]) silently ignores any threads that
            // can't fit into the array
            threadCountActual = tg.enumerate(threads);
        }

        return threads;
    }

    private void clearThreadLocalMap(ClassLoader cl, Object map,
            Field internalTableField) throws NoSuchMethodException,
            IllegalAccessException, NoSuchFieldException,
            InvocationTargetException {
        if (map != null) {
            Method mapRemove = map.getClass().getDeclaredMethod("remove",
                    ThreadLocal.class);
            mapRemove.setAccessible(true);
            Object[] table = (Object[]) internalTableField.get(map);
            if (table != null) {
                for (int j = 0; j < table.length; j++) {
                    if (table[j] != null) {
                        boolean remove = false;
                        // Check the key
                        Field keyField = Reference.class
                                .getDeclaredField("referent");
                        keyField.setAccessible(true);
                        Object key = keyField.get(table[j]);
                        if (cl.equals(key)
                                || (key != null && cl == key.getClass()
                                        .getClassLoader())) {
                            remove = true;
                        }
                        // Check the value
                        Field valueField = table[j].getClass()
                                .getDeclaredField("value");
                        valueField.setAccessible(true);
                        Object value = valueField.get(table[j]);
                        if (cl.equals(value)
                                || (value != null && cl == value.getClass()
                                        .getClassLoader())) {
                            remove = true;
                        }
                        if (remove) {
                            Object entry = ((Reference<?>) table[j]).get();
                            if (logger.isInfoEnabled()) {
                                logger.info("Removing " + key.toString()
                                        + " from a thread local...");
                            }
                            mapRemove.invoke(map, entry);
                        }
                    }
                }
            }
        }
    }

上記のコードで、スレッドが終了するのを待ち、Tomcatが処理する前にスレッド系のものたちをクリーンするのでエラーが出なくなる(TimerThreadを使っているものがあれば、上記のコードは未対応なので処理が必要)。Tomcatがやりたい、メモリリークをしないようにというのは分かるのだけど、これを 6.0.24 からエラーレベルログは吐くのは間違っているんじゃないかね…。WARNくらいなレベルな気がするのだけど。しかも、シャットダウンする時なので、そもそも全部捨てるから吐かれてもな、という気がする(再配備するようなケースではこの処理があると有効かも)。

今回はアプリ側で直したけど、S2とかのライブラリ側で対応するとなると、ThreadLocalをextendedしないとか、ThreadLocalは使い終わりにremove()を呼んでおくとか、そういう対応になるような気が。たとえば、S2のHttpServletExternalContextとか、スレッドローカルがあるけど、removeな状態にしておくのは問題がある気がするし。しかも、スレッドローカルだから消すのがめんどい…。そんな感じで、そもそもの解決策は、どうせ値を消しているのだから、Tomcatエラーレベルログを吐かなければ良いだけじゃないかね。

そのうち、みんなが 6.0.24 を使い始めて文句が多発で変更が入りそうな気がする動きだな…。

koichikkoichik 2010/02/11 18:00 Seasara2 の方は対応しました.[CONTAINER-413]
SVN コミット済み&Maven SNAPSHOT リポジトリにデプロイ済みです.

JDBC ドライバの登録解除と TimeoutManager スレッドの停止は jdbc-extension.dicon に定義したコンポーネントで行うようにしたので,jdbc.dicon でインクルード済みならそのままでエラーは出なくなるはずです.
SMART deploy を使っていない (jdbc-extension.dicon をインクルードできない
) 場合の設定は JIRA の方に書いておきました (ドキュメントにも書いてますがまだ公開できない).

> ThreadLocalをextendedしない

そうしました.

> ThreadLocalは使い終わりにremove()を呼んでおく

残念ながら ThreadLocal#remove() は Java5 で追加されたメソッドで, Seasar2 本体は未だに (未来永劫) J2SE1.4 対応なのでした.orz

ともあれ (JW),お試し頂けると助かります.

shinsuke_sugayashinsuke_sugaya 2010/02/11 22:12 お〜、ありがとうございます。
S2DbcpCleanerの方で試してみました。
S2関連のclearReferencesたちが出なくなりましたー。
ThreadLocal#remove() は Java5 からだったのですね…。それは未確認でした。

koichikkoichik 2010/02/12 00:01 確認ありがとうございました.

koichikkoichik 2010/02/12 18:00 ごめんなさい,このやり方だと Derby を使った JUnit のテストケースが動かなくなるので,S2DbcpCleaner は廃止して S2ContainerServlet/S2ContainerListener で後片付けをすることにしました.なので,S2DbcpCleaner の設定は削除してください.
修正版は SVN コミット&Maven SNAPSHOT リポジトリにデプロイ済みです.

shinsuke_sugayashinsuke_sugaya 2010/02/12 18:19 ありがとうございます!
了解ですー。