{"record":{"id":"409923b3555397a8","repo":"Tencent/matrix","slug":"plugin-destroy-but-plugin-has-been-already-destro","errorCode":null,"errorMessage":"plugin destroy, but plugin has been already destroyed","messagePattern":"plugin destroy, but plugin has been already destroyed","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/plugin/Plugin.java","lineNumber":138,"sourceCode":"            throw new RuntimeException(\"plugin stop, but plugin is never started\");\n        }\n\n        status = PLUGIN_STOPPED;\n\n        if (pluginListener == null) {\n            throw new RuntimeException(\"plugin stop, plugin listener is null\");\n        }\n        pluginListener.onStop(this);\n    }\n\n    @Override\n    public void destroy() {\n        // stop first\n        if (isPluginStarted()) {\n            stop();\n        }\n        if (isPluginDestroyed()) {\n            throw new RuntimeException(\"plugin destroy, but plugin has been already destroyed\");\n        }\n        status = PLUGIN_DESTROYED;\n\n        if (pluginListener == null) {\n            throw new RuntimeException(\"plugin destroy, plugin listener is null\");\n        }\n        pluginListener.onDestroy(this);\n    }\n\n    @Override\n    public String getTag() {\n        return getClass().getName();\n    }\n\n    @Override\n    public void onForeground(boolean isForeground) {\n\n    }","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/Tencent/matrix/blob/3b8293bd65d47eeea7caf1f32a3a5d4d5eab60e7/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/plugin/Plugin.java#L120-L156","documentation":"Plugin.destroy() throws this when the plugin's status is already PLUGIN_DESTROYED. Destroy is meant to be a terminal, one-time lifecycle operation; calling it twice indicates a lifecycle management bug, so Matrix throws instead of silently double-destroying.","triggerScenarios":"Calling plugin.destroy() a second time on the same instance, e.g. both Matrix.onDestroy()/destroy() and application-level cleanup code destroy the same plugin, or destroy() is invoked after a previous destroy.","commonSituations":"Double teardown paths (Activity.onDestroy plus Application termination hook); retry logic calling destroy again after a failure; holding stale plugin references in a registry and destroying them in multiple shutdown handlers.","solutions":["Check isPluginDestroyed() before calling destroy()","Ensure only one code path owns plugin teardown (e.g. Matrix.onDestroy only)","Track destroyed plugins in a Set and skip already-destroyed instances","Wrap destroy() in try-catch for RuntimeException during shutdown where failure should not crash the app"],"exampleFix":"// before\nplugin.destroy();\nplugin.destroy(); // throws\n\n// after\nif (!plugin.isPluginDestroyed()) {\n    plugin.destroy();\n}","handlingStrategy":"validation","validationCode":"if (plugin.isPluginDestroyed()) {\n    return; // already torn down\n}\nplugin.destroy();","typeGuard":"if (plugin != null && !plugin.isPluginDestroyed()) { plugin.destroy(); }","tryCatchPattern":"try {\n    plugin.destroy();\n} catch (RuntimeException e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"already destroyed\")) {\n        Log.d(TAG, \"plugin already destroyed, ignoring\");\n    } else { throw e; }\n}","preventionTips":["Centralize plugin teardown in a single owner (Matrix.onDestroy)","Track destroyed instances in a Set before destroying","Avoid calling destroy from multiple shutdown hooks"],"tags":["android","lifecycle","double-destroy","plugin"],"backgroundTag":"invalid-state-transition","analyzedSha":"3b8293bd65d47eeea7caf1f32a3a5d4d5eab60e7","analyzedAt":"2026-09-08T08:01:39.722Z","contentChangedAt":"2026-09-08T08:01:39.722Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}