From 684ff77d097a1637be2311e75e13bfeba1e28a0c Mon Sep 17 00:00:00 2001 From: "shan.wu" Date: Mon, 3 Aug 2026 16:24:45 +0800 Subject: [PATCH] [plugin]: prevent concurrent plugin driver registration PROBLEM: APIRefreshPluginDriversMsg refreshes all management nodes concurrently. On first load, multiple nodes can observe a missing PluginDriverVO and insert the same UUID, causing duplicate-key failures. The persistence unit also still references the obsolete PluginDriverVO package. SOLUTION: Register the current PluginDriverVO class in the persistence unit. Serialize metadata upserts with a per-plugin database GLock and query the row again after acquiring the lock. Update the local driver registry only after the shared metadata write succeeds. TESTING: The database-backed lock path was exercised remotely with two concurrent callers. CI validation is pending. Resolves: ZSTAC-87773 Change-Id: I886da86d2cd46d8a52138643e1cd5ae49df52fcc --- conf/persistence.xml | 2 +- .../zstack/core/plugin/PluginManagerImpl.java | 70 ++++++++++++------- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/conf/persistence.xml b/conf/persistence.xml index e76141af3ad..d1cc8727ce9 100755 --- a/conf/persistence.xml +++ b/conf/persistence.xml @@ -14,7 +14,7 @@ org.zstack.core.job.JobQueueEntryVO org.zstack.core.config.GlobalConfigVO org.zstack.core.eventlog.EventLogVO - org.zstack.core.plugin.PluginDriverVO + org.zstack.header.core.external.plugin.PluginDriverVO org.zstack.resourceconfig.ResourceConfigVO org.zstack.header.managementnode.ManagementNodeVO org.zstack.header.managementnode.ManagementNodeContextVO diff --git a/core/src/main/java/org/zstack/core/plugin/PluginManagerImpl.java b/core/src/main/java/org/zstack/core/plugin/PluginManagerImpl.java index e26f419b468..ae1562bc043 100644 --- a/core/src/main/java/org/zstack/core/plugin/PluginManagerImpl.java +++ b/core/src/main/java/org/zstack/core/plugin/PluginManagerImpl.java @@ -14,6 +14,7 @@ import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.CloudBusCallBack; import org.zstack.core.db.DatabaseFacade; +import org.zstack.core.db.GLock; import org.zstack.core.db.Q; import org.zstack.core.db.SQL; import org.zstack.core.errorcode.ErrorFacade; @@ -44,8 +45,10 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -57,6 +60,7 @@ */ public class PluginManagerImpl extends AbstractService implements PluginManager { private static final CLogger logger = Utils.getLogger(PluginManagerImpl.class); + private static final long PLUGIN_DRIVER_REGISTRATION_LOCK_TIMEOUT = TimeUnit.MINUTES.toSeconds(2); @Autowired private DatabaseFacade dbf; @@ -135,42 +139,58 @@ protected void registerPluginAsSingleton( verifyPluginProduct(pluginDriver); + persistPluginDriver(pluginDriver); + pluginInstances.put(pluginDriver.uuid(), pluginDriver); List registeredPlugins = pluginRegisters.computeIfAbsent( pluginDriverClz, k -> new ArrayList<>()); registeredPlugins.removeIf(registered -> Objects.equals( registered.uuid(), pluginDriver.uuid())); registeredPlugins.add(pluginDriver); - - PluginDriverVO vo = dbf.findByUuid(pluginDriver.uuid(), PluginDriverVO.class); - if (vo == null) { - vo = new PluginDriverVO(); - vo.setUuid(pluginDriver.uuid()); - vo.setName(pluginDriver.name()); - vo.setVendor(pluginDriver.vendor()); - vo.setFeatures(JSONObjectUtil.toJsonString(pluginDriver.features())); - vo.setType(pluginDriver.type()); - vo.setDescription(pluginDriver.description()); - vo.setVersion(pluginDriver.version()); - vo.setLicense(pluginDriver.license()); - vo.setOptionTypes(JSONObjectUtil.toJsonString(pluginDriver.optionTypes())); - dbf.persist(vo); - } else { - vo.setName(pluginDriver.name()); - vo.setVendor(pluginDriver.vendor()); - vo.setFeatures(JSONObjectUtil.toJsonString(pluginDriver.features())); - vo.setType(pluginDriver.type()); - vo.setDescription(pluginDriver.description()); - vo.setVersion(pluginDriver.version()); - vo.setLicense(pluginDriver.license()); - vo.setOptionTypes(JSONObjectUtil.toJsonString(pluginDriver.optionTypes())); - dbf.update(vo); - } } catch (Exception e) { throw new CloudRuntimeException(e); } } + protected void persistPluginDriver(PluginDriver pluginDriver) { + GLock lock = createPluginDriverRegistrationLock(pluginDriver.uuid()); + lock.lock(); + try { + upsertPluginDriver(pluginDriver); + } finally { + lock.unlock(); + } + } + + protected GLock createPluginDriverRegistrationLock(String pluginUuid) { + String lockUuid = UUID.nameUUIDFromBytes(pluginUuid.getBytes(StandardCharsets.UTF_8)).toString(); + return new GLock(String.format("plugin-driver-%s", lockUuid), + PLUGIN_DRIVER_REGISTRATION_LOCK_TIMEOUT, dbf); + } + + protected void upsertPluginDriver(PluginDriver pluginDriver) { + PluginDriverVO vo = dbf.findByUuid(pluginDriver.uuid(), PluginDriverVO.class); + boolean newPlugin = vo == null; + if (newPlugin) { + vo = new PluginDriverVO(); + vo.setUuid(pluginDriver.uuid()); + } + + vo.setName(pluginDriver.name()); + vo.setVendor(pluginDriver.vendor()); + vo.setFeatures(JSONObjectUtil.toJsonString(pluginDriver.features())); + vo.setType(pluginDriver.type()); + vo.setDescription(pluginDriver.description()); + vo.setVersion(pluginDriver.version()); + vo.setLicense(pluginDriver.license()); + vo.setOptionTypes(JSONObjectUtil.toJsonString(pluginDriver.optionTypes())); + if (newPlugin) { + dbf.persist(vo); + } else { + dbf.update(vo); + } + } + private void getPluginInterfaceSingletons(Class abstractPluginClz) { Platform.getReflections() .getSubTypesOf(abstractPluginClz)