#chiroito ’s blog

Java を中心とした趣味の技術について

OpenJDK開発記:JDK-8219904: ClassCastException when calling FlightRecorderMXBean#getRecordings()を直してみた。

これは何?

今回、パッチを書いたのはJDK-8219904ClassCastException when calling FlightRecorderMXBean#getRecordings()です。

[JDK-8219904] ClassCastException when calling FlightRecorderMXBean#getRecordings() - Java Bug System

これはJMXのリモート接続でJFRの一覧を取得しようとするとClassCastExceptionが発生するというバグです。

どんな実装だったのか

原因としてはリモートでJMXで接続してJFRの一覧を取得しようとするとCompositeDataからデータを取り出してRecordingInfoを作ります。その時に上記例外が発生します。具体的にはidの型がlongなのにintにキャストしてClassCastExceptionは起きます。また、仕様上toDiskという要素名なのに、実装はdiskという名前になっているためそこでも問題が発生します。 原因としては、ローカル接続でのテストはやっていましたが、リモート接続でのテストをやっていなかったことです。

どう直したの?

基本的には、型と名前を変えるだけでOK。ただし、このバグが直っていないJMXクライアントからdiskという要素名を使って接続してくる可能性があるため、そこも注意する必要があります。 また、再発防止のため、自プロセスへリモート接続をしてJFRの記録を起動/一覧を取得/停止するテストを追加しました。しかし、これは普通にやると自分自身にはアタッチできないため実行できません。そのため、自分自身にアタッチできるようにするため-Djdk.attach.allowAttachSelf=trueというVM引数を追加します。

パッチはこちら

パッチの抜粋はこちらです。

--- a/src/jdk.management.jfr/share/classes/jdk/management/jfr/RecordingInfo.java Mon Feb 24 23:24:14 2020 +0100
+++ b/src/jdk.management.jfr/share/classes/jdk/management/jfr/RecordingInfo.java  Tue Feb 25 03:28:31 2020 +0100
@@ -1,5 +1,5 @@
//略
@@ -51,7 +51,7 @@
     private final String state;
     private final boolean dumpOnExit;
     private final long size;
-    private final boolean disk;
+    private final boolean toDisk;
     private final long maxAge;
     private final long maxSize;
     private final long startTime;
@@ -67,7 +67,7 @@
         state = recording.getState().toString();
         dumpOnExit = recording.getDumpOnExit();
         size = recording.getSize();
-        disk = recording.isToDisk();
+        toDisk = recording.isToDisk();
 
         Duration d = recording.getMaxAge();
         if (d == null) {
@@ -87,12 +87,17 @@
     }
 
     private RecordingInfo(CompositeData cd) {
-        id = (int) cd.get("id");
+        id = (long) cd.get("id");
         name = (String) cd.get("name");
         state = (String) cd.get("state");
         dumpOnExit = (boolean) cd.get("dumpOnExit");
         size = (long) cd.get("size");
-        disk = (boolean) cd.get("disk");
+        if(cd.containsKey("toDisk")){
+            toDisk = (boolean) cd.get("toDisk");
+        } else {
+            // Before JDK-8219904 was fixed, the element name was disk, so for compatibility
+            toDisk = (boolean) cd.get("disk");
+        }
         maxAge = (Long) cd.get("maxAge");
         maxSize = (Long) cd.get("maxSize");
         startTime = (Long) cd.get("startTime");
@@ -290,7 +295,7 @@
      * @return {@code true} if recording is to disk, {@code false} otherwise
      */
     public boolean isToDisk() {
-        return disk;
+        return toDisk;
     }
 
     /**
@@ -342,7 +347,7 @@
      * <td>{@code Long}</td>
      * </tr>
      * <tr>
-     * <th scope="row">disk</th>
+     * <th scope="row">toDisk</th>
      * <td>{@code Boolean}</td>
      * </tr>
      * <tr>
--- a/test/jdk/jdk/jfr/jmx/JmxHelper.java Mon Feb 24 23:24:14 2020 +0100
+++ b/test/jdk/jdk/jfr/jmx/JmxHelper.java Tue Feb 25 03:28:31 2020 +0100
@@ -36,6 +36,7 @@
//略
@@ -52,7 +53,15 @@
//略
+
 public class JmxHelper {
+    private static final String LOCAL_CONNECTION_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress";
 
     public static RecordingInfo getJmxRecording(long recId) {
         for (RecordingInfo r : getFlighteRecorderMXBean().getRecordings()) {
@@ -279,4 +288,18 @@
         return ManagementFactory.getPlatformMXBean(FlightRecorderMXBean.class);
     }
 
+    public static long getPID(){
+        return ManagementFactory.getRuntimeMXBean().getPid();
+    }
+
+    public static FlightRecorderMXBean getFlighteRecorderMXBean(long pid) throws Exception {
+        VirtualMachine targetVM = VirtualMachine.attach("" + pid);
+        String jmxServiceUrl = targetVM.getAgentProperties().getProperty(LOCAL_CONNECTION_ADDRESS);
+        JMXServiceURL jmxURL = new JMXServiceURL(jmxServiceUrl);
+        JMXConnector connector = JMXConnectorFactory.connect(jmxURL);
+        MBeanServerConnection connection = connector.getMBeanServerConnection();
+
+        ObjectName objectName = new ObjectName("jdk.management.jfr:type=FlightRecorder");
+        return JMX.newMXBeanProxy(connection, objectName, FlightRecorderMXBean.class);
+    }
 }
--- a/test/jdk/jdk/jfr/jmx/TestGetRecordings.java Mon Feb 24 23:24:14 2020 +0100
+++ b/test/jdk/jdk/jfr/jmx/TestGetRecordings.java Tue Feb 25 03:28:31 2020 +0100
@@ -1,5 +1,5 @@
//略
@@ -35,7 +35,7 @@
  * @key jfr
  * @requires vm.hasJFR
  * @library /test/lib /test/jdk
- * @run main/othervm jdk.jfr.jmx.TestGetRecordings
+ * @run main/othervm -Djdk.attach.allowAttachSelf=true -Dcom.sun.management.jmxremote jdk.jfr.jmx.TestGetRecordings
  */
 public class TestGetRecordings {
     public static void main(String[] args) throws Throwable {
@@ -46,5 +46,11 @@
         JmxHelper.verifyNotExists(recId, preCreateRecordings);
         bean.closeRecording(recId);
         JmxHelper.verifyNotExists(recId, bean.getRecordings());
+
+        long selfPID = JmxHelper.getPID();
+        FlightRecorderMXBean remoteBean = JmxHelper.getFlighteRecorderMXBean(selfPID);
+        long remoteRecId = remoteBean.newRecording();
+        remoteBean.getRecordings();
+        remoteBean.closeRecording(remoteRecId);
     }
 }