From dee38dc32ebb3927bec7fe285b316bdb237f6f91 Mon Sep 17 00:00:00 2001 From: Chuansheng Lu Date: Fri, 3 Apr 2020 15:21:58 +0800 Subject: [PATCH] [MultiTenant] Added TenantDataIsolation feature Summary: ported TenantDataIsolation to Dragonwell Test Plan: jdk/test/multi-tenant Reviewed-by: yuleil, superajun-wsj Issue: https://github.com/alibaba/dragonwell8/issues/84 --- .../com/alibaba/tenant/FieldReference.java | 27 ++++ .../com/alibaba/tenant/TenantContainer.java | 53 +++++++ .../com/alibaba/tenant/TenantData.java | 87 ++++++++++++ .../com/alibaba/tenant/TenantGlobals.java | 13 ++ src/share/classes/java/lang/ClassValue.java | 33 ++++- src/share/classes/java/lang/Runtime.java | 16 ++- src/share/classes/java/lang/System.java | 44 +++++- .../lang/management/ManagementFactory.java | 68 +++++---- .../classes/java/lang/ref/Finalizer.java | 114 ++++++++++++--- src/share/classes/java/sql/DriverManager.java | 24 +++- .../classes/java/util/ResourceBundle.java | 32 +++-- .../javax/management/MBeanServerFactory.java | 19 ++- src/share/classes/sun/misc/SharedSecrets.java | 9 ++ src/share/classes/sun/misc/TenantAccess.java | 15 ++ .../TestStaticFieldIsolation.java | 131 ++++++++++++++++++ .../test/com/alibaba/tenant/TestJMX.java | 95 +++++++++++++ 16 files changed, 698 insertions(+), 82 deletions(-) create mode 100644 src/share/classes/com/alibaba/tenant/FieldReference.java create mode 100644 src/share/classes/com/alibaba/tenant/TenantData.java create mode 100644 src/share/classes/sun/misc/TenantAccess.java create mode 100644 test/multi-tenant/TestStaticFieldIsolation.java create mode 100644 test/multi-tenant/test/com/alibaba/tenant/TestJMX.java diff --git a/src/share/classes/com/alibaba/tenant/FieldReference.java b/src/share/classes/com/alibaba/tenant/FieldReference.java new file mode 100644 index 000000000..a7db927ef --- /dev/null +++ b/src/share/classes/com/alibaba/tenant/FieldReference.java @@ -0,0 +1,27 @@ +package com.alibaba.tenant; + +public class FieldReference { + private T referent; + /** + * Constructs a new FieldReference with r as the referent + * @param r The object to which this FieldReference points + */ + public FieldReference(T r) { + this.referent = r; + } + /** + * Returns this FieldReference referent + * @return The referent + */ + public T get() { + return referent; + } + + /** + * Sets {@code referent} + * @param t The new referent value + */ + public void set(Object t) { + referent = (T)t; + } +} \ No newline at end of file diff --git a/src/share/classes/com/alibaba/tenant/TenantContainer.java b/src/share/classes/com/alibaba/tenant/TenantContainer.java index cc25dd189..5f5cb125e 100644 --- a/src/share/classes/com/alibaba/tenant/TenantContainer.java +++ b/src/share/classes/com/alibaba/tenant/TenantContainer.java @@ -36,6 +36,8 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; import com.alibaba.rcm.ResourceContainer; import com.alibaba.rcm.Constraint; import com.alibaba.rcm.internal.AbstractResourceContainer; +import sun.misc.SharedSecrets; +import sun.misc.TenantAccess; import static com.alibaba.tenant.TenantState.*; @@ -136,11 +138,24 @@ public class TenantContainer { return totalThreadsAllocatedMemory + accumulatedMemory; } + /** + * Data repository used to store the data isolated per tenant. + */ + private TenantData tenantData = new TenantData(); + /** * Used to track and run tenant shutdown hooks */ private TenantShutdownHooks tenantShutdownHooks = new TenantShutdownHooks(); + /** + * Retrieves the data repository used by this tenant. + * @return the data repository associated with this tenant. + */ + public TenantData getTenantData() { + return tenantData; + } + /** * Sets the tenant properties to the one specified by argument. * @param props the properties to be set, CoW the system properties if it is null. @@ -266,6 +281,7 @@ public class TenantContainer { // clear references spawnedThreads.clear(); attachedThreads.clear(); + tenantData.clear(); tenantShutdownHooks = null; } @@ -584,6 +600,18 @@ public class TenantContainer { //Initialize this field after the system is booted. tenantContainerMap = Collections.synchronizedMap(new HashMap()); + // initialize TenantAccess + if (SharedSecrets.getTenantAccess() == null) { + SharedSecrets.setTenantAccess(new TenantAccess() { + @Override + public void registerServiceThread(TenantContainer tenant, Thread thread) { + if (tenant != null && thread != null) { + tenant.addServiceThread(thread); + } + } + }); + } + try { // force initialization of TenantConfiguration Class.forName("com.alibaba.tenant.TenantConfiguration"); @@ -605,6 +633,31 @@ public class TenantContainer { return obj != null ? nd.containerOf(obj) : null; } + /** + * Gets the field value stored in the data repository of this tenant, which is same to call the + * {@code TenantData.getFieldValue} on the tenant data object retrieved by {@code TenantContainer.getTenantData}. + * + * @param obj Object the field associates with + * @param fieldName Field name + * @param supplier Responsible for creating the initial field value + * @return Value of field. + */ + public T getFieldValue(K obj, String fieldName, Supplier supplier) { + return tenantData.getFieldValue(obj, fieldName, supplier); + } + + /** + * Gets the field value stored in the data repository of this tenant, which is same to call the + * {@code TenantData.getFieldValue} on the tenant data object retrieved by {@code TenantContainer.getTenantData}. + * + * @param obj Object the field associates with + * @param fieldName Field name + * @return Value of field, null if not found + */ + public T getFieldValue(K obj, String fieldName) { + return getFieldValue(obj, fieldName, () -> null); + } + /** * Runs {@code Supplier.get} in the root tenant. * @param supplier target used to call diff --git a/src/share/classes/com/alibaba/tenant/TenantData.java b/src/share/classes/com/alibaba/tenant/TenantData.java new file mode 100644 index 000000000..e596b9541 --- /dev/null +++ b/src/share/classes/com/alibaba/tenant/TenantData.java @@ -0,0 +1,87 @@ +package com.alibaba.tenant; + +import java.util.HashMap; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.function.Supplier; + +/** + * Used to store all the data isolated per tenant. + */ +public class TenantData { + private Map>> dataMap = new WeakHashMap<>(); + + private Map> mapForObject(Object obj) { + Map> map = null; + if (obj != null) { + map = dataMap.get(obj); + if (null == map) { + map = new HashMap<>(); + dataMap.put(obj, map); + } + } + return map; + } + + /** + * Retrieves the value of {@code obj}'s field isolated per tenant + * @param obj Object the field associates with + * @param fieldName Field name + * @param supplier Responsible for creating the initial field value + * @return Value of field. + */ + @SuppressWarnings("unchecked") + public synchronized T getFieldValue(K obj, String fieldName, Supplier supplier) { + if(null == obj || null == fieldName || null == supplier) { + throw new IllegalArgumentException("Failed to get the field value, argument is null."); + } + + Map> objMap = mapForObject(obj); + FieldReference data = objMap.get(fieldName); + if(null == data) { + T initialValue = supplier.get(); + // Only store non-null value to the map + if (initialValue != null) { + data = new FieldReference(initialValue); + objMap.put(fieldName, data); + } + } + return (data == null ? null : (T)data.get()); + } + + /** + * Retrieves the value of {@code obj}'s field isolated per tenant + * @param obj Object the field associates with + * @param fieldName Field name + * @return Value of field, null if not found + */ + public synchronized T getFieldValue(K obj, String fieldName) { + return getFieldValue(obj, fieldName, () -> null); + } + + /** + * Sets the value for tenant isolated field + * @param key Object associated with field. + * @param fieldName name of field. + * @param value new field value + */ + public synchronized void setFieldValue(K key, String fieldName, T value) { + if(null == key || null == fieldName) { + throw new IllegalArgumentException("Failed to get the field value, argument is null."); + } + Map> objMap = mapForObject(key); + FieldReference ref = objMap.get(fieldName); + if(null != ref) { + ref.set(value); + } else { + objMap.put(fieldName, new FieldReference<>(value)); + } + } + + /** + * Clear all recorded isolation data in this tenant + */ + void clear() { + dataMap.clear(); + } +} diff --git a/src/share/classes/com/alibaba/tenant/TenantGlobals.java b/src/share/classes/com/alibaba/tenant/TenantGlobals.java index 51e1c6b22..7cf5c74d8 100644 --- a/src/share/classes/com/alibaba/tenant/TenantGlobals.java +++ b/src/share/classes/com/alibaba/tenant/TenantGlobals.java @@ -51,6 +51,11 @@ public class TenantGlobals { */ public static final int TENANT_FLAG_CPU_THROTTLING_ENABLED = 0x4; + /** + * Bit to indicate that if data isolation feature is enabled + */ + public static final int TENANT_FLAG_DATA_ISOLATION_ENABLED = 0x8; + /** * Bit to indicate that if cpu accounting feature is enabled */ @@ -103,4 +108,12 @@ public class TenantGlobals { public static boolean isCpuAccountingEnabled() { return 0 != (flags & TENANT_FLAG_CPU_ACCOUNTING_ENABLED); } + + /** + * Test if data isolation feature is enabled. + * @return true if enabled otherwise false + */ + public static boolean isDataIsolationEnabled() { + return 0 != (flags & TENANT_FLAG_DATA_ISOLATION_ENABLED); + } } diff --git a/src/share/classes/java/lang/ClassValue.java b/src/share/classes/java/lang/ClassValue.java index e58634b15..c5e27b593 100644 --- a/src/share/classes/java/lang/ClassValue.java +++ b/src/share/classes/java/lang/ClassValue.java @@ -25,6 +25,9 @@ package java.lang; +import com.alibaba.tenant.TenantContainer; +import com.alibaba.tenant.TenantGlobals; + import java.lang.ClassValue.ClassValueMap; import java.util.WeakHashMap; import java.lang.ref.WeakReference; @@ -186,7 +189,13 @@ public abstract class ClassValue { /** Return the cache, if it exists, else a dummy empty cache. */ private static Entry[] getCacheCarefully(Class type) { // racing type.classValueMap{.cacheArray} : null => new Entry[X] <=> new Entry[Y] - ClassValueMap map = type.classValueMap; + ClassValueMap map = null; + if (TenantGlobals.isDataIsolationEnabled() && TenantContainer.current() != null) { + map = TenantContainer.current().getFieldValue(type, "classValueMap", + () -> new ClassValueMap(type)); + } else { + map = type.classValueMap; + } if (map == null) return EMPTY_CACHE; Entry[] cache = map.getCache(); return cache; @@ -364,7 +373,13 @@ public abstract class ClassValue { // racing type.classValueMap : null (blank) => unique ClassValueMap // if a null is observed, a map is created (lazily, synchronously, uniquely) // all further access to that map is synchronized - ClassValueMap map = type.classValueMap; + ClassValueMap map = null; + if (TenantGlobals.isDataIsolationEnabled() && TenantContainer.current() != null) { + map = TenantContainer.current().getFieldValue(type, "classValueMap", + () -> new ClassValueMap(type)); + } else { + map = type.classValueMap; + } if (map != null) return map; return initializeMap(type); } @@ -374,11 +389,17 @@ public abstract class ClassValue { ClassValueMap map; synchronized (CRITICAL_SECTION) { // private object to avoid deadlocks // happens about once per type - if ((map = type.classValueMap) == null) - type.classValueMap = map = new ClassValueMap(type); - } - return map; + if (TenantGlobals.isDataIsolationEnabled() && TenantContainer.current() != null) { + type.classValueMap = map = TenantContainer.current() + .getFieldValue(type, "classValueMap", () -> new ClassValueMap(type)); + } else { + if ((map = type.classValueMap) == null) { + type.classValueMap = map = new ClassValueMap(type); + } + } } + return map; + } static Entry makeEntry(Version explicitVersion, T value) { // Note that explicitVersion might be different from this.version. diff --git a/src/share/classes/java/lang/Runtime.java b/src/share/classes/java/lang/Runtime.java index 503905914..a203eb041 100644 --- a/src/share/classes/java/lang/Runtime.java +++ b/src/share/classes/java/lang/Runtime.java @@ -28,6 +28,10 @@ package java.lang; import java.io.*; import java.util.StringTokenizer; + +import com.alibaba.tenant.TenantContainer; +import com.alibaba.tenant.TenantGlobals; + import sun.reflect.CallerSensitive; import sun.reflect.Reflection; @@ -209,7 +213,11 @@ public class Runtime { if (sm != null) { sm.checkPermission(new RuntimePermission("shutdownHooks")); } - ApplicationShutdownHooks.add(hook); + if(TenantGlobals.isDataIsolationEnabled() && null != TenantContainer.current()) { + TenantContainer.current().addShutdownHook(hook); + } else { + ApplicationShutdownHooks.add(hook); + } } /** @@ -237,7 +245,11 @@ public class Runtime { if (sm != null) { sm.checkPermission(new RuntimePermission("shutdownHooks")); } - return ApplicationShutdownHooks.remove(hook); + if(TenantGlobals.isDataIsolationEnabled() && null != TenantContainer.current()) { + return TenantContainer.current().removeShutdownHook(hook); + } else { + return ApplicationShutdownHooks.remove(hook); + } } /** diff --git a/src/share/classes/java/lang/System.java b/src/share/classes/java/lang/System.java index 1ddd39c24..551a29c06 100644 --- a/src/share/classes/java/lang/System.java +++ b/src/share/classes/java/lang/System.java @@ -38,6 +38,9 @@ import java.security.AllPermission; import java.nio.channels.Channel; import java.nio.channels.spi.SelectorProvider; import com.alibaba.rcm.internal.AbstractResourceContainer; +import com.alibaba.tenant.TenantContainer; +import com.alibaba.tenant.TenantGlobals; +import sun.misc.VM; import sun.nio.ch.Interruptible; import sun.reflect.CallerSensitive; import sun.reflect.Reflection; @@ -632,8 +635,11 @@ public final class System { if (sm != null) { sm.checkPropertiesAccess(); } - - return props; + if(TenantGlobals.isDataIsolationEnabled() && null != TenantContainer.current()) { + return TenantContainer.current().getProperties(); + } else { + return props; + } } /** @@ -684,7 +690,12 @@ public final class System { props = new Properties(); initProperties(props); } - System.props = props; + if (VM.isBooted() && TenantGlobals.isDataIsolationEnabled() + && null != TenantContainer.current()) { + TenantContainer.current().setProperties(props); + } else { + System.props = props; + } } /** @@ -720,7 +731,12 @@ public final class System { sm.checkPropertyAccess(key); } - return props.getProperty(key); + if(VM.isBooted() && TenantGlobals.isDataIsolationEnabled() + && null != TenantContainer.current()) { + return TenantContainer.current().getProperty(key); + } else { + return props.getProperty(key); + } } /** @@ -756,7 +772,12 @@ public final class System { sm.checkPropertyAccess(key); } - return props.getProperty(key, def); + if (VM.isBooted() && TenantGlobals.isDataIsolationEnabled() + && null != TenantContainer.current()) { + return TenantContainer.current().getProperties().getProperty(key, def); + } else { + return props.getProperty(key, def); + } } /** @@ -796,7 +817,12 @@ public final class System { SecurityConstants.PROPERTY_WRITE_ACTION)); } - return (String) props.setProperty(key, value); + if(VM.isBooted() && TenantGlobals.isDataIsolationEnabled() + && null != TenantContainer.current()) { + return (String) TenantContainer.current().setProperty(key, value); + } else { + return (String) props.setProperty(key, value); + } } /** @@ -833,7 +859,11 @@ public final class System { sm.checkPermission(new PropertyPermission(key, "write")); } - return (String) props.remove(key); + if(TenantGlobals.isDataIsolationEnabled() && null != TenantContainer.current()) { + return (String) TenantContainer.current().clearProperty(key); + } else { + return (String) props.remove(key); + } } private static void checkKey(String key) { diff --git a/src/share/classes/java/lang/management/ManagementFactory.java b/src/share/classes/java/lang/management/ManagementFactory.java index c44471354..00a65b647 100644 --- a/src/share/classes/java/lang/management/ManagementFactory.java +++ b/src/share/classes/java/lang/management/ManagementFactory.java @@ -30,7 +30,6 @@ import javax.management.MBeanServerConnection; import javax.management.MBeanServerFactory; import javax.management.MBeanServerPermission; import javax.management.NotificationEmitter; -import javax.management.ObjectInstance; import javax.management.ObjectName; import javax.management.InstanceAlreadyExistsException; import javax.management.InstanceNotFoundException; @@ -39,6 +38,8 @@ import javax.management.MBeanRegistrationException; import javax.management.NotCompliantMBeanException; import javax.management.StandardEmitterMBean; import javax.management.StandardMBean; +import com.alibaba.tenant.TenantContainer; +import com.alibaba.tenant.TenantGlobals; import java.util.Collections; import java.util.List; import java.util.Set; @@ -465,38 +466,51 @@ public class ManagementFactory { sm.checkPermission(perm); } - if (platformMBeanServer == null) { - platformMBeanServer = MBeanServerFactory.createMBeanServer(); - for (PlatformComponent pc : PlatformComponent.values()) { - List list = - pc.getMXBeans(pc.getMXBeanInterface()); - for (PlatformManagedObject o : list) { - // Each PlatformComponent represents one management - // interface. Some MXBean may extend another one. - // The MXBean instances for one platform component - // (returned by pc.getMXBeans()) might be also - // the MXBean instances for another platform component. - // e.g. com.sun.management.GarbageCollectorMXBean - // - // So need to check if an MXBean instance is registered - // before registering into the platform MBeanServer - if (!platformMBeanServer.isRegistered(o.getObjectName())) { - addMXBean(platformMBeanServer, o); - } - } - } - HashMap dynmbeans = - ManagementFactoryHelper.getPlatformDynamicMBeans(); - for (Map.Entry e : dynmbeans.entrySet()) { - addDynamicMBean(platformMBeanServer, e.getValue(), e.getKey()); + MBeanServer thePlatformMBeanServer= null; + if(TenantGlobals.isDataIsolationEnabled() && null != TenantContainer.current()) { + thePlatformMBeanServer = TenantContainer.current().getFieldValue(ManagementFactory.class, "platformMBeanServer", + ManagementFactory::createPlatformMBeanServer); + } else { + //use the value in root. + if (platformMBeanServer == null) { + platformMBeanServer = createPlatformMBeanServer(); } - for (final PlatformManagedObject o : - ExtendedPlatformComponent.getMXBeans()) { + thePlatformMBeanServer = platformMBeanServer; + } + return thePlatformMBeanServer; + } + + private static MBeanServer createPlatformMBeanServer() { + MBeanServer platformMBeanServer = MBeanServerFactory.createMBeanServer(); + for (PlatformComponent pc : PlatformComponent.values()) { + List list = + pc.getMXBeans(pc.getMXBeanInterface()); + for (PlatformManagedObject o : list) { + // Each PlatformComponent represents one management + // interface. Some MXBean may extend another one. + // The MXBean instances for one platform component + // (returned by pc.getMXBeans()) might be also + // the MXBean instances for another platform component. + // e.g. com.sun.management.GarbageCollectorMXBean + // + // So need to check if an MXBean instance is registered + // before registering into the platform MBeanServer if (!platformMBeanServer.isRegistered(o.getObjectName())) { addMXBean(platformMBeanServer, o); } } } + HashMap dynmbeans = + ManagementFactoryHelper.getPlatformDynamicMBeans(); + for (Map.Entry e : dynmbeans.entrySet()) { + addDynamicMBean(platformMBeanServer, e.getValue(), e.getKey()); + } + for (final PlatformManagedObject o : + ExtendedPlatformComponent.getMXBeans()) { + if (!platformMBeanServer.isRegistered(o.getObjectName())) { + addMXBean(platformMBeanServer, o); + } + } return platformMBeanServer; } diff --git a/src/share/classes/java/lang/ref/Finalizer.java b/src/share/classes/java/lang/ref/Finalizer.java index 5cc0ac76a..ef2f4a15e 100644 --- a/src/share/classes/java/lang/ref/Finalizer.java +++ b/src/share/classes/java/lang/ref/Finalizer.java @@ -27,9 +27,14 @@ package java.lang.ref; import java.security.PrivilegedAction; import java.security.AccessController; + +import com.alibaba.tenant.TenantState; import sun.misc.JavaLangAccess; import sun.misc.SharedSecrets; import sun.misc.VM; +import com.alibaba.tenant.TenantContainer; +import com.alibaba.tenant.TenantData; +import com.alibaba.tenant.TenantGlobals; final class Finalizer extends FinalReference { /* Package-private; must be in same package as the Reference @@ -39,31 +44,75 @@ final class Finalizer extends FinalReference { /* Package-private; must private static Finalizer unfinalized = null; private static final Object lock = new Object(); + private static final String ID_UNFINALIZED = "unfinalized"; + private Finalizer next = null, prev = null; + private static ReferenceQueue initTenantReferenceQueue() { + if (!TenantGlobals.isDataIsolationEnabled() || TenantContainer.current() == null) { + throw new UnsupportedOperationException(); + } + // spawn a new finalizer thread for this tenant, put it into system thread group + // will be terminated after destruction of tenant container + ThreadGroup tg = Thread.currentThread().getThreadGroup(); + for (ThreadGroup tgn = tg; + tgn != null; + tg = tgn, tgn = tg.getParent()); + Thread finalizer = new FinalizerThread(tg); + SharedSecrets.getTenantAccess() + .registerServiceThread(TenantContainer.current(), finalizer); + finalizer.setName("TenantFinalizer-" + TenantContainer.current().getTenantId()); + finalizer.setPriority(Thread.MAX_PRIORITY - 2); + finalizer.setDaemon(true); + finalizer.start(); + + return new ReferenceQueue<>(); + } + private boolean hasBeenFinalized() { return (next == this); } private void add() { synchronized (lock) { - if (unfinalized != null) { - this.next = unfinalized; - unfinalized.prev = this; + if (VM.isBooted() && TenantGlobals.isDataIsolationEnabled() && TenantContainer.current() != null) { + TenantData td = TenantContainer.current().getTenantData(); + Finalizer tenantUnfinalized = td.getFieldValue(Finalizer.class, ID_UNFINALIZED); + if (tenantUnfinalized != null) { + this.next = tenantUnfinalized; + tenantUnfinalized.prev = this; + } + td.setFieldValue(Finalizer.class, ID_UNFINALIZED, this); + } else { + if (unfinalized != null) { + this.next = unfinalized; + unfinalized.prev = this; + } + unfinalized = this; } - unfinalized = this; } } private void remove() { synchronized (lock) { - if (unfinalized == this) { - if (this.next != null) { - unfinalized = this.next; - } else { - unfinalized = this.prev; + if (TenantGlobals.isDataIsolationEnabled() && TenantContainer.current() != null) { + TenantData td = TenantContainer.current().getTenantData(); + if (td.getFieldValue(Finalizer.class, ID_UNFINALIZED) == this) { + if (this.next != null) { + td.setFieldValue(Finalizer.class, ID_UNFINALIZED, this.next); + } else { + td.setFieldValue(Finalizer.class, ID_UNFINALIZED, this.prev); + } + } + } else { + if (unfinalized == this) { + if (this.next != null) { + unfinalized = this.next; + } else { + unfinalized = this.prev; + } } } if (this.next != null) { @@ -78,12 +127,17 @@ final class Finalizer extends FinalReference { /* Package-private; must } private Finalizer(Object finalizee) { - super(finalizee, queue); + super(finalizee, getQueue()); add(); } static ReferenceQueue getQueue() { - return queue; + if (VM.isBooted() && TenantGlobals.isDataIsolationEnabled() && TenantContainer.current() != null) { + return TenantContainer.current().getFieldValue(Finalizer.class, "queue", + Finalizer::initTenantReferenceQueue); + } else { + return queue; + } } /* Invoked by VM */ @@ -131,6 +185,12 @@ final class Finalizer extends FinalReference { /* Package-private; must tgn != null; tg = tgn, tgn = tg.getParent()); Thread sft = new Thread(tg, proc, "Secondary finalizer"); + + if (TenantGlobals.isDataIsolationEnabled() && TenantContainer.current() != null) { + SharedSecrets.getTenantAccess() + .registerServiceThread(TenantContainer.current(), sft); + } + sft.start(); try { sft.join(); @@ -155,8 +215,9 @@ final class Finalizer extends FinalReference { /* Package-private; must return; final JavaLangAccess jla = SharedSecrets.getJavaLangAccess(); running = true; + ReferenceQueue q = getQueue(); for (;;) { - Finalizer f = (Finalizer)queue.poll(); + Finalizer f = (Finalizer)q.poll(); if (f == null) break; f.runFinalizer(jla); } @@ -180,10 +241,19 @@ final class Finalizer extends FinalReference { /* Package-private; must running = true; for (;;) { Finalizer f; - synchronized (lock) { - f = unfinalized; - if (f == null) break; - unfinalized = f.next; + if (TenantGlobals.isDataIsolationEnabled() && TenantContainer.current() != null) { + TenantData td = TenantContainer.current().getTenantData(); + synchronized (lock) { + f = td.getFieldValue(Finalizer.class, ID_UNFINALIZED); + if (f == null) break; + td.setFieldValue(Finalizer.class, ID_UNFINALIZED, f.next); + } + } else { + synchronized (lock) { + f = unfinalized; + if (f == null) break; + unfinalized = f.next; + } } f.runFinalizer(jla); }}}); @@ -211,13 +281,21 @@ final class Finalizer extends FinalReference { /* Package-private; must } final JavaLangAccess jla = SharedSecrets.getJavaLangAccess(); running = true; - for (;;) { + ReferenceQueue q = getQueue(); + for (;q != null;) { try { - Finalizer f = (Finalizer)queue.remove(); + Finalizer f = (Finalizer)q.remove(); f.runFinalizer(jla); } catch (InterruptedException x) { // ignore and continue } + + // terminate Finalizer thread proactively after TenantContainer.destroy() + if (TenantGlobals.isDataIsolationEnabled() + && TenantContainer.current() != null + && TenantContainer.current().getState() == TenantState.DEAD) { + break; + } } } } diff --git a/src/share/classes/java/sql/DriverManager.java b/src/share/classes/java/sql/DriverManager.java index a9190a485..1efbf037e 100644 --- a/src/share/classes/java/sql/DriverManager.java +++ b/src/share/classes/java/sql/DriverManager.java @@ -32,6 +32,8 @@ import java.security.PrivilegedAction; import java.util.concurrent.CopyOnWriteArrayList; import sun.reflect.CallerSensitive; import sun.reflect.Reflection; +import com.alibaba.tenant.TenantContainer; +import com.alibaba.tenant.TenantGlobals; /** @@ -92,6 +94,13 @@ public class DriverManager { /* Prevent the DriverManager class from being instantiated. */ private DriverManager(){} + private static CopyOnWriteArrayList getRegisteredDrivers() { + if (TenantGlobals.isDataIsolationEnabled() && TenantContainer.current() != null) { + return TenantContainer.current().getFieldValue(DriverManager.class, "registeredDrivers", + () -> new CopyOnWriteArrayList<>()); + } + return registeredDrivers; + } /** * Load the initial JDBC drivers by checking the System property @@ -291,7 +300,7 @@ public class DriverManager { // Walk through the loaded registeredDrivers attempting to locate someone // who understands the given URL. - for (DriverInfo aDriver : registeredDrivers) { + for (DriverInfo aDriver : getRegisteredDrivers()) { // If the caller does not have permission to load the driver then // skip it. if(isDriverAllowed(aDriver.driver, callerClass)) { @@ -355,7 +364,7 @@ public class DriverManager { /* Register the driver if it has not already been added to our list */ if(driver != null) { - registeredDrivers.addIfAbsent(new DriverInfo(driver, da)); + getRegisteredDrivers().addIfAbsent(new DriverInfo(driver, da)); } else { // This is for compatibility with the original DriverManager throw new NullPointerException(); @@ -405,15 +414,16 @@ public class DriverManager { println("DriverManager.deregisterDriver: " + driver); DriverInfo aDriver = new DriverInfo(driver, null); - if(registeredDrivers.contains(aDriver)) { + CopyOnWriteArrayList drivers = getRegisteredDrivers(); + if (drivers.contains(aDriver)) { if (isDriverAllowed(driver, Reflection.getCallerClass())) { - DriverInfo di = registeredDrivers.get(registeredDrivers.indexOf(aDriver)); + DriverInfo di = drivers.get(drivers.indexOf(aDriver)); // If a DriverAction was specified, Call it to notify the // driver that it has been deregistered if(di.action() != null) { di.action().deregister(); } - registeredDrivers.remove(aDriver); + drivers.remove(aDriver); } else { // If the caller does not have permission to load the driver then // throw a SecurityException. @@ -440,7 +450,7 @@ public class DriverManager { Class callerClass = Reflection.getCallerClass(); // Walk through the loaded registeredDrivers. - for(DriverInfo aDriver : registeredDrivers) { + for (DriverInfo aDriver : getRegisteredDrivers()) { // If the caller does not have permission to load the driver then // skip it. if(isDriverAllowed(aDriver.driver, callerClass)) { @@ -655,7 +665,7 @@ public class DriverManager { // Remember the first exception that gets raised so we can reraise it. SQLException reason = null; - for(DriverInfo aDriver : registeredDrivers) { + for (DriverInfo aDriver : getRegisteredDrivers()) { // If the caller does not have permission to load the driver then // skip it. if(isDriverAllowed(aDriver.driver, callerCL)) { diff --git a/src/share/classes/java/util/ResourceBundle.java b/src/share/classes/java/util/ResourceBundle.java index 54aee0ef3..2d9b9c9a9 100644 --- a/src/share/classes/java/util/ResourceBundle.java +++ b/src/share/classes/java/util/ResourceBundle.java @@ -57,6 +57,8 @@ import java.util.concurrent.ConcurrentMap; import java.util.jar.JarEntry; import java.util.spi.ResourceBundleControlProvider; +import com.alibaba.tenant.TenantContainer; +import com.alibaba.tenant.TenantGlobals; import sun.reflect.CallerSensitive; import sun.reflect.Reflection; import sun.util.locale.BaseLocale; @@ -312,6 +314,14 @@ public abstract class ResourceBundle { private static final ConcurrentMap cacheList = new ConcurrentHashMap<>(INITIAL_CACHE_SIZE); + private static ConcurrentMap getCacheList() { + if (TenantGlobals.isDataIsolationEnabled() && TenantContainer.current() != null) { + return TenantContainer.current().getFieldValue(ResourceBundle.class, "cacheList", + () -> new ConcurrentHashMap<>(INITIAL_CACHE_SIZE)); + } + return cacheList; + } + /** * Queue for reference objects referring to class loaders or bundles. */ @@ -1334,7 +1344,7 @@ public abstract class ResourceBundle { ResourceBundle bundle = null; // Quick lookup of the cache. - BundleReference bundleRef = cacheList.get(cacheKey); + BundleReference bundleRef = getCacheList().get(cacheKey); if (bundleRef != null) { bundle = bundleRef.get(); bundleRef = null; @@ -1445,7 +1455,7 @@ public abstract class ResourceBundle { // information from the cache. Object ref; while ((ref = referenceQueue.poll()) != null) { - cacheList.remove(((CacheKeyReference)ref).getCacheKey()); + getCacheList().remove(((CacheKeyReference)ref).getCacheKey()); } // flag indicating the resource bundle has expired in the cache @@ -1468,9 +1478,9 @@ public abstract class ResourceBundle { } // Otherwise, remove the cached one since we can't keep // the same bundles having different parents. - BundleReference bundleRef = cacheList.get(cacheKey); + BundleReference bundleRef = getCacheList().get(cacheKey); if (bundleRef != null && bundleRef.get() == bundle) { - cacheList.remove(cacheKey, bundleRef); + getCacheList().remove(cacheKey, bundleRef); } } } @@ -1597,7 +1607,7 @@ public abstract class ResourceBundle { */ private static ResourceBundle findBundleInCache(CacheKey cacheKey, Control control) { - BundleReference bundleRef = cacheList.get(cacheKey); + BundleReference bundleRef = getCacheList().get(cacheKey); if (bundleRef == null) { return null; } @@ -1644,7 +1654,7 @@ public abstract class ResourceBundle { assert bundle != NONEXISTENT_BUNDLE; bundle.expired = true; bundle.cacheKey = null; - cacheList.remove(cacheKey, bundleRef); + getCacheList().remove(cacheKey, bundleRef); bundle = null; } else { CacheKey key = bundleRef.getCacheKey(); @@ -1675,7 +1685,7 @@ public abstract class ResourceBundle { // return the bundle with the expired flag // on. bundle.cacheKey = null; - cacheList.remove(cacheKey, bundleRef); + getCacheList().remove(cacheKey, bundleRef); } else { // Update the expiration control info. and reuse // the same bundle instance @@ -1685,7 +1695,7 @@ public abstract class ResourceBundle { } } else { // We just remove NONEXISTENT_BUNDLE from the cache. - cacheList.remove(cacheKey, bundleRef); + getCacheList().remove(cacheKey, bundleRef); bundle = null; } } @@ -1712,7 +1722,7 @@ public abstract class ResourceBundle { bundle.cacheKey = key; // Put the bundle in the cache if it's not been in the cache. - BundleReference result = cacheList.putIfAbsent(key, bundleRef); + BundleReference result = getCacheList().putIfAbsent(key, bundleRef); // If someone else has put the same bundle in the cache before // us and it has not expired, we should use the one in the cache. @@ -1728,7 +1738,7 @@ public abstract class ResourceBundle { } else { // Replace the invalid (garbage collected or expired) // instance with the valid one. - cacheList.put(key, bundleRef); + getCacheList().put(key, bundleRef); } } } @@ -1776,7 +1786,7 @@ public abstract class ResourceBundle { if (loader == null) { throw new NullPointerException(); } - Set set = cacheList.keySet(); + Set set = getCacheList().keySet(); for (CacheKey key : set) { if (key.getLoader() == loader) { set.remove(key); diff --git a/src/share/classes/javax/management/MBeanServerFactory.java b/src/share/classes/javax/management/MBeanServerFactory.java index 7eade40ba..284a95b24 100644 --- a/src/share/classes/javax/management/MBeanServerFactory.java +++ b/src/share/classes/javax/management/MBeanServerFactory.java @@ -35,6 +35,8 @@ import java.util.ArrayList; import java.util.logging.Level; import javax.management.loading.ClassLoaderRepository; import sun.reflect.misc.ReflectUtil; +import com.alibaba.tenant.TenantContainer; +import com.alibaba.tenant.TenantGlobals; /** @@ -361,10 +363,10 @@ public class MBeanServerFactory { checkPermission("findMBeanServer"); if (agentId == null) - return new ArrayList(mBeanServerList); + return new ArrayList(getMBeanServerList()); ArrayList result = new ArrayList(); - for (MBeanServer mbs : mBeanServerList) { + for (MBeanServer mbs : getMBeanServerList()) { String name = mBeanServerId(mbs); if (agentId.equals(name)) result.add(mbs); @@ -415,11 +417,11 @@ public class MBeanServerFactory { } private static synchronized void addMBeanServer(MBeanServer mbs) { - mBeanServerList.add(mbs); + getMBeanServerList().add(mbs); } private static synchronized void removeMBeanServer(MBeanServer mbs) { - boolean removed = mBeanServerList.remove(mbs); + boolean removed = getMBeanServerList().remove(mbs); if (!removed) { MBEANSERVER_LOGGER.logp(Level.FINER, MBeanServerFactory.class.getName(), @@ -432,6 +434,15 @@ public class MBeanServerFactory { private static final ArrayList mBeanServerList = new ArrayList(); + private static ArrayList getMBeanServerList() { + if (TenantGlobals.isDataIsolationEnabled() && TenantContainer.current() != null) { + return TenantContainer.current().getFieldValue(MBeanServerFactory.class, "mBeanServerList", + () -> new ArrayList<>()); + } else { + return mBeanServerList; + } + } + /** * Load the builder class through the context class loader. * @param builderClassName The name of the builder class. diff --git a/src/share/classes/sun/misc/SharedSecrets.java b/src/share/classes/sun/misc/SharedSecrets.java index d046e4b19..39c62a35c 100644 --- a/src/share/classes/sun/misc/SharedSecrets.java +++ b/src/share/classes/sun/misc/SharedSecrets.java @@ -61,6 +61,7 @@ public class SharedSecrets { private static JavaxCryptoSealedObjectAccess javaxCryptoSealedObjectAccess; private static JavaObjectInputStreamReadString javaObjectInputStreamReadString; private static JavaObjectInputStreamAccess javaObjectInputStreamAccess; + private static TenantAccess tenantAccess; public static JavaUtilJarAccess javaUtilJarAccess() { if (javaUtilJarAccess == null) { @@ -235,4 +236,12 @@ public class SharedSecrets { } return javaxCryptoSealedObjectAccess; } + + public static void setTenantAccess(TenantAccess access) { + tenantAccess = access; + } + + public static TenantAccess getTenantAccess() { + return tenantAccess; + } } diff --git a/src/share/classes/sun/misc/TenantAccess.java b/src/share/classes/sun/misc/TenantAccess.java new file mode 100644 index 000000000..fd19cf1a0 --- /dev/null +++ b/src/share/classes/sun/misc/TenantAccess.java @@ -0,0 +1,15 @@ +package sun.misc; + +import com.alibaba.tenant.TenantContainer; + +public interface TenantAccess { + + /** + * Register a thread to {@code tenant}'s service thread list. + * At present, a service thread is either a registered shutdown hook thread or Finalizer thread. + * + * @param tenant The teant container to register thread with + * @param thread Thread to be registered + */ + void registerServiceThread(TenantContainer tenant, Thread thread); +} diff --git a/test/multi-tenant/TestStaticFieldIsolation.java b/test/multi-tenant/TestStaticFieldIsolation.java new file mode 100644 index 000000000..d8f3c3de2 --- /dev/null +++ b/test/multi-tenant/TestStaticFieldIsolation.java @@ -0,0 +1,131 @@ + +/* + * @test + * @summary Test isolation of various static fields + * @library /lib/testlibrary + * @run main/othervm -XX:+UseG1GC -XX:+MultiTenant -XX:+TenantDataIsolation TestStaticFieldIsolation + */ + +import static jdk.testlibrary.Asserts.*; +import java.lang.reflect.Field; +import java.util.ResourceBundle; +import java.util.concurrent.atomic.AtomicInteger; +import javax.management.MBeanServerFactory; +import com.alibaba.tenant.TenantConfiguration; +import com.alibaba.tenant.TenantContainer; +import com.alibaba.tenant.TenantException; + + +public class TestStaticFieldIsolation { + + public static void main(String[] args) throws TenantException { + TestStaticFieldIsolation test = new TestStaticFieldIsolation(); + test.testIsolation_java_lang_ResourceBundle_cacheList(); + test.testIsolation_java_sql_DriverManager_registeredDrivers(); + test.testIsolation_javax_management_MBeanServerFactory_mBeanServerList(); + test.testIsolation_java_lang_Class_classValueMap(); + } + + private void testIsolation_java_lang_ResourceBundle_cacheList() throws TenantException { + Runnable task = () -> { + try { + ResourceBundle.getBundle("NON_EXIST"); + } catch (Throwable t) { /* ignore */ } + }; + testStaticFieldIsolation(ResourceBundle.class, task, "cacheList"); + } + + private void testIsolation_javax_management_MBeanServerFactory_mBeanServerList() throws TenantException { + Runnable task = () -> { + try { + MBeanServerFactory.findMBeanServer(null); + } catch (Throwable t) { /* ignore */ } + }; + testStaticFieldIsolation(MBeanServerFactory.class, task, "mBeanServerList"); + } + + private void testIsolation_java_sql_DriverManager_registeredDrivers() throws TenantException { + Runnable task = () -> { + try { + java.sql.DriverManager.getDrivers(); + } catch (Throwable t) { /* ignore */ } + }; + testStaticFieldIsolation(java.sql.DriverManager.class, task, "registeredDrivers"); + } + + class TestClass { + int val; + } + + private void testIsolation_java_lang_Class_classValueMap() throws TenantException { + final AtomicInteger atomicInteger = new AtomicInteger(0); + + ClassValue value = new ClassValue() { + @Override protected TestClass computeValue(Class type) { + TestClass obj = new TestClass(); + obj.val = atomicInteger.getAndIncrement(); + return obj; + } + }; + + TestClass tc = value.get(TestClass.class); + assertEquals(tc.val, 0); + TestClass tc2 = value.get(TestClass.class); + assertTrue(tc == tc2); + + TestClass[] receivers = new TestClass[2]; + TenantContainer tenant = createSimpleTenant(); + tenant.run(() -> { + receivers[0] = value.get(TestClass.class); + }); + assertEquals(receivers[0].val, 1); + + tenant.run(() -> { + receivers[1] = value.get(TestClass.class); + }); + assertEquals(receivers[1].val, 1); + assertTrue(receivers[1] == receivers[0]); + assertTrue(receivers[0] != tc); + + + } + + /** + * test isolation of {@code classKay}'s static field {@code fieldName} + * @param classKey The class contains target static field + * @param tenantTask Task to trigger the static filed isolation per tenant, + * which is stored in TenantContainer.tenantData + * @param fieldName Field name + * @throws TenantException + */ + private static void testStaticFieldIsolation(Class classKey, Runnable tenantTask, String fieldName) + throws TenantException { + TenantContainer tenant = createSimpleTenant(); + + assertNull(tenant.getFieldValue(classKey, fieldName)); + + tenant.run(tenantTask); + + Object isolatedFieldValue = tenant.getFieldValue(classKey, fieldName); + assertNotNull(isolatedFieldValue); + + try { + Field field = classKey.getDeclaredField(fieldName); + field.setAccessible(true); + Object rootValue = field.get(null); + + assertTrue(rootValue != isolatedFieldValue); + } catch (NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + fail(); + } + + tenant.destroy(); + } + + // convenient static method to create a simple {@code TenantContainer} object + private static TenantContainer createSimpleTenant() { + TenantConfiguration config = new TenantConfiguration(1024, 64 * 1024 * 1024); + return TenantContainer.create(config); + } +} diff --git a/test/multi-tenant/test/com/alibaba/tenant/TestJMX.java b/test/multi-tenant/test/com/alibaba/tenant/TestJMX.java new file mode 100644 index 000000000..6b49bf6b8 --- /dev/null +++ b/test/multi-tenant/test/com/alibaba/tenant/TestJMX.java @@ -0,0 +1,95 @@ +package test.com.alibaba.tenant; +import static jdk.testlibrary.Asserts.*; +import java.lang.management.ManagementFactory; +import java.util.Set; + +import javax.management.MBeanInfo; +import javax.management.MBeanServer; +import javax.management.MBeanServerInvocationHandler; +import javax.management.ObjectInstance; +import javax.management.ObjectName; +import javax.management.StandardMBean; +import org.junit.Test; +import com.alibaba.tenant.TenantConfiguration; +import com.alibaba.tenant.TenantContainer; +import com.alibaba.tenant.TenantException; + +/* @test + * @summary JMX related unit tests + * @library /lib/testlibrary + * @compile TestJMX.java + * @run junit/othervm/timeout=300 -XX:+MultiTenant -XX:+TenantDataIsolation -XX:+TenantHeapThrottling -XX:+UseG1GC + * -XX:+TenantCpuThrottling -Xmx600m -Xms600m test.com.alibaba.tenant.TestJMX + */ +public class TestJMX { + public static interface MXBean { + public String getName(); + } + + public class MXBeanImpl implements MXBean { + public String getName() { + return "test"; + } + } + + private void registerAndVerifyMBean(MBeanServer mbs) { + try { + ObjectName myInfoObj = new ObjectName("com.alibaba.tenant.mxbean:type=MyTest"); + MXBeanImpl myMXBean = new MXBeanImpl(); + StandardMBean smb = new StandardMBean(myMXBean, MXBean.class); + mbs.registerMBean(smb, myInfoObj); + + assertTrue(mbs.isRegistered(myInfoObj)); + + //call the method of MXBean + MXBean mbean = + (MXBean)MBeanServerInvocationHandler.newProxyInstance( + mbs,new ObjectName("com.alibaba.tenant.mxbean:type=MyTest"), MXBean.class, true); + assertTrue("test".equals(mbean.getName())); + + Set instances = mbs.queryMBeans(new ObjectName("com.alibaba.tenant.mxbean:type=MyTest"), null); + ObjectInstance instance = (ObjectInstance) instances.toArray()[0]; + assertTrue(myMXBean.getClass().getName().equals(instance.getClassName())); + + MBeanInfo info = mbs.getMBeanInfo(myInfoObj); + assertTrue(myMXBean.getClass().getName().equals(info.getClassName())); + } catch (Exception e) { + e.printStackTrace(); + fail(); + } + } + + @Test + public void testMBeanServerIsolation() { + //verify in root. + registerAndVerifyMBean(ManagementFactory.getPlatformMBeanServer()); + + TenantConfiguration tconfig = new TenantConfiguration(1024, 100 * 1024 * 1024); + final TenantContainer tenant = TenantContainer.create(tconfig); + final TenantContainer tenant2 = TenantContainer.create(tconfig); + + try { + tenant.run(() -> { + //verify in the tenant 1. + registerAndVerifyMBean(ManagementFactory.getPlatformMBeanServer()); + }); + } catch (TenantException e) { + e.printStackTrace(); + fail(); + } + + try { + tenant2.run(() -> { + //verify in the tenant 1. + registerAndVerifyMBean(ManagementFactory.getPlatformMBeanServer()); + }); + } catch (TenantException e) { + e.printStackTrace(); + fail(); + } + } + + public static void main(String[] args) throws Exception { + new TestJMX().testMBeanServerIsolation(); + } +} -- GitLab