Author: woodroy
Date: Mon Mar 23 17:14:56 2009
New Revision: 757453
URL: http://svn.apache.org/viewvc?rev=757453&view=rev
Log:
Update initCache and clearCache to account for multi-threaded case.
Essentially, one thread could change the static 'resolvedSchema' variable from
null to non-null after another thread found the value to be null.
Contributor: Jeff Barrett via Roy Wood
Added:
webservices/commons/trunk/modules/XmlSchema/src/test/java/tests/SchemaBuilderCacheTest.java
Modified:
webservices/commons/trunk/modules/XmlSchema/src/main/java/org/apache/ws/commons/schema/SchemaBuilder.java
Modified:
webservices/commons/trunk/modules/XmlSchema/src/main/java/org/apache/ws/commons/schema/SchemaBuilder.java
URL:
http://svn.apache.org/viewvc/webservices/commons/trunk/modules/XmlSchema/src/main/java/org/apache/ws/commons/schema/SchemaBuilder.java?rev=757453&r1=757452&r2=757453&view=diff
==============================================================================
---
webservices/commons/trunk/modules/XmlSchema/src/main/java/org/apache/ws/commons/schema/SchemaBuilder.java
(original)
+++
webservices/commons/trunk/modules/XmlSchema/src/main/java/org/apache/ws/commons/schema/SchemaBuilder.java
Mon Mar 23 17:14:56 2009
@@ -24,6 +24,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -66,7 +67,8 @@
* initialize resolvedSchemas to non-null Clearing of cache is done by
calling clearCache() which will
* clear and nullify resolvedSchemas
*/
- private static Map<String, SoftReference<XmlSchema>> resolvedSchemas;
+
+ private static Map<String, Map<String, SoftReference<XmlSchema>>>
resolvedSchemas;
private static final String[] RESERVED_ATTRIBUTES_LIST = {"name",
"type",
"default",
@@ -109,16 +111,108 @@
currentSchema = new XmlSchema();
}
+
+ /**
+ * Setup the cache to be used by the current thread of execution. Multiple
+ * threads can use the cache, and each one must call this method at
+ * some point prior to attempting to resolve the first schema, or the cache
+ * will not be used on that thread.
+ *
+ * IMPORTANT: The thread MUST call clearCache() when it is done with the
+ * schemas or a large amount of memory may remain in-use.
+ */
+ public static synchronized void initCache() {
+
+ if (resolvedSchemas == null) {
+ resolvedSchemas =
+ Collections.synchronizedMap(new HashMap<String, Map<String,
SoftReference<XmlSchema>>>());
+ }
+
+ String threadID = String.valueOf(Thread.currentThread().getId());
+
+ Map<String, SoftReference<XmlSchema>> threadResolvedSchemas =
+ resolvedSchemas.get(threadID);
+
+ // If there is no entry yet for this thread ID, then create one
+ if (threadResolvedSchemas == null) {
+ threadResolvedSchemas = Collections.synchronizedMap(new
Hashtable<String, SoftReference<XmlSchema>>());
+ resolvedSchemas.put(threadID, threadResolvedSchemas);
+
+ }
+ }
+
+ /**
+ * Remove any entries from the cache for the current thread. Entries for
+ * other threads are not altered.
+ */
public static synchronized void clearCache() {
if (resolvedSchemas != null) {
- resolvedSchemas.clear(); // necessary?
- resolvedSchemas = null;
+ String threadID = String.valueOf(Thread.currentThread().getId());
+ // If there are entries for this thread ID, then clear them and
+ // remove the entry
+ Map<String, SoftReference<XmlSchema>> threadResolvedSchemas =
+ resolvedSchemas.get(threadID);
+
+ if (threadResolvedSchemas != null) {
+ threadResolvedSchemas.clear();
+ resolvedSchemas.remove(threadID);
+ }
+ }
+ }
+
+ /**
+ * Return a cached schema if one exists for this thread. In order for
schemas to be cached
+ * the thread must have done an initCache() previously.
+ * The parameters are used to construct a key used to lookup the schema
+ * @param targetNamespace
+ * @param schemaLocation
+ * @param baseUri
+ * @return The cached schema if one exists for this thread or null.
+ */
+ private XmlSchema getCachedSchema(String targetNamespace,
+ String schemaLocation, String baseUri) {
+
+ XmlSchema resolvedSchema = null;
+
+ if (resolvedSchemas != null) { // cache is initialized, use it
+ String threadID = String.valueOf(Thread.currentThread().getId());
+ Map<String, SoftReference<XmlSchema>> threadResolvedSchemas =
+ resolvedSchemas.get(threadID);
+ if (threadResolvedSchemas != null) {
+ // Not being very smart about this at the moment. One could,
for example,
+ // see that the schemaLocation or baseUri is the same as
another, but differs
+ // only by a trailing slash. As it is now, we assume a single
character difference
+ // means it's a schema that has yet to be resolved.
+ String schemaKey = targetNamespace + schemaLocation + baseUri;
+ SoftReference<XmlSchema> softref =
threadResolvedSchemas.get(schemaKey);
+ if (softref != null) {
+ resolvedSchema = softref.get();
+ }
+ }
}
+ return resolvedSchema;
}
- public static synchronized void initCache() {
- if (resolvedSchemas == null) {
- resolvedSchemas = Collections.synchronizedMap(new HashMap<String,
SoftReference<XmlSchema>>());
+ /**
+ * Add an XmlSchema to the cache if the current thread has the cache
enabled.
+ * The first three parameters are used to construct a key
+ * @param targetNamespace
+ * @param schemaLocation
+ * @param baseUri
+ * This parameter is the value put under the key (if the cache is enabled)
+ * @param readSchema
+ */
+ private void putCachedSchema(String targetNamespace, String schemaLocation,
+ String baseUri, XmlSchema readSchema) {
+
+ if (resolvedSchemas != null) {
+ String threadID = String.valueOf(Thread.currentThread().getId());
+ Map<String, SoftReference<XmlSchema>> threadResolvedSchemas =
+ resolvedSchemas.get(threadID);
+ if (threadResolvedSchemas != null) {
+ String schemaKey = targetNamespace + schemaLocation + baseUri;
+ threadResolvedSchemas.put(schemaKey, new
SoftReference<XmlSchema>(readSchema));
+ }
}
}
@@ -917,22 +1011,10 @@
XmlSchema resolveXmlSchema(String targetNamespace, String schemaLocation,
String baseUri,
TargetNamespaceValidator validator) {
- String schemaKey = null;
- if (resolvedSchemas != null) { // cache is initialized, use it
- // Not being very smart about this at the moment. One could, for
example,
- // see that the schemaLocation or baseUri is the same as another,
but differs
- // only by a trailing slash. As it is now, we assume a single
character difference
- // means it's a schema that has yet to be resolved.
- schemaKey = Thread.currentThread().getId() + targetNamespace +
schemaLocation + baseUri;
- SoftReference<XmlSchema> softref = resolvedSchemas.get(schemaKey);
- if (softref != null) {
- XmlSchema resolvedSchema = softref.get();
- if (resolvedSchema != null) {
- return resolvedSchema;
- }
- }
+ if (getCachedSchema(targetNamespace, schemaLocation, baseUri) != null)
{
+ return getCachedSchema(targetNamespace, schemaLocation, baseUri);
}
-
+
// use the entity resolver provided if the schema location is present
null
if (schemaLocation != null && !"".equals(schemaLocation)) {
InputSource source =
collection.getSchemaResolver().resolveEntity(targetNamespace,
@@ -958,9 +1040,7 @@
collection.push(key);
try {
XmlSchema readSchema = collection.read(source, null,
validator);
- if (resolvedSchemas != null) {
- resolvedSchemas.put(schemaKey, new
SoftReference<XmlSchema>(readSchema));
- }
+ putCachedSchema(targetNamespace, schemaLocation, baseUri,
readSchema);
return readSchema;
} catch (Exception e) {
throw new RuntimeException(e);
Added:
webservices/commons/trunk/modules/XmlSchema/src/test/java/tests/SchemaBuilderCacheTest.java
URL:
http://svn.apache.org/viewvc/webservices/commons/trunk/modules/XmlSchema/src/test/java/tests/SchemaBuilderCacheTest.java?rev=757453&view=auto
==============================================================================
---
webservices/commons/trunk/modules/XmlSchema/src/test/java/tests/SchemaBuilderCacheTest.java
(added)
+++
webservices/commons/trunk/modules/XmlSchema/src/test/java/tests/SchemaBuilderCacheTest.java
Mon Mar 23 17:14:56 2009
@@ -0,0 +1,424 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+
+package tests;
+
+import java.lang.ref.SoftReference;
+import java.lang.reflect.Field;
+import java.util.Hashtable;
+import java.util.Map;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.apache.ws.commons.schema.SchemaBuilder;
+import org.apache.ws.commons.schema.XmlSchema;
+import org.apache.ws.commons.schema.XmlSchemaCollection;
+import org.w3c.dom.Document;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.Ignore;
+
+/**
+ * Test the resolved Schema cache.
+ */
+public class SchemaBuilderCacheTest extends Assert {
+
+ /**
+ * Test that if the cache is not initialized, then it should not be used
when a schema is read.
+ * @throws Exception
+ */
+ @Test
+ public void testResolveCacheUninitialized() throws Exception {
+ Document doc = setupDocument();
+ XmlSchemaCollection schemaCol = setupXmlSchemaCollection();
+ XmlSchema schema = schemaCol.read(doc,null);
+ assertNotNull(schema);
+
+ // If the cache is not in use, then it should be null
+ assertNull(getResolvedSchemasHashtable());
+ }
+
+ /**
+ * Test if the cache is initialized it will be populated when a schema is
read and it will
+ * be cleared when the clearCache method is called.
+ * @throws Exception
+ */
+ @Test
+ public void testResolveCacheInitialized() throws Exception {
+ try {
+ SchemaBuilder.initCache();
+ Document doc = setupDocument();
+ XmlSchemaCollection schemaCol = setupXmlSchemaCollection();
+ XmlSchema schema = schemaCol.read(doc, null);
+ assertNotNull(schema);
+
+ // If the cache is in use, it should not be null and there should
+ // be an entry for this thread ID
+ assertNotNull(getResolvedSchemasHashtable());
+ Map<String, SoftReference<XmlSchema>> threadHT =
getThreadResolvedSchemaHashtable();
+ assertNotNull(threadHT);
+ assertFalse(threadHT.isEmpty());
+ assertEquals(1, threadHT.size());
+
+ // After clearing the cache, there should be no entry for this
thread ID, and
+ // the hashtable should not be null
+ SchemaBuilder.clearCache();
+ assertNotNull(getResolvedSchemasHashtable());
+ assertNull(getThreadResolvedSchemaHashtable());
+ System.out.println("Line 13");
+ } finally {
+ resetResolvedSchemasHashtable();
+ }
+
+ // If the cache is enabled, then it should be non-null
+ }
+
+ /**
+ * Test that threads can not affect the cache for other threads.
+ */
+ @Test
+ public void testMultithreadCache() {
+ try {
+ MultithreadUpdateLockMonitor testMonitor = new
MultithreadUpdateLockMonitor();
+ startupTestThreads(testMonitor);
+
+ if (testMonitor.t1Exception != null) {
+ fail("Thread T1 encountred an error: "
+ + testMonitor.t1Exception.toString());
+ }
+ if (testMonitor.t2Exception != null) {
+ fail("Thread T2 encountred an error: "
+ + testMonitor.t2Exception.toString());
+ }
+ if (testMonitor.t3Exception != null) {
+ fail("Thread T3 encountred an error: "
+ + testMonitor.t3Exception.toString());
+ }
+ } finally {
+ resetResolvedSchemasHashtable();
+ }
+ }
+
+
//==============================================================================================
+ // Utility Methods
+
//==============================================================================================
+
+ static Document setupDocument() {
+ DocumentBuilderFactory documentBuilderFactory =
DocumentBuilderFactory.newInstance();
+ documentBuilderFactory.setNamespaceAware(true);
+ Document doc;
+ try {
+ doc = documentBuilderFactory.newDocumentBuilder().
+ parse(Resources.asURI("importBase.xsd"));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ return doc;
+ }
+
+ static XmlSchemaCollection setupXmlSchemaCollection() {
+ XmlSchemaCollection schemaCol = new XmlSchemaCollection();
+ schemaCol.setBaseUri(Resources.TEST_RESOURCES);
+ return schemaCol;
+ }
+
+ /**
+ * Override the protection on the resolvedSchemas attribute in the
SchemaBuilder class
+ * and return its current value.
+ * @return value of Hashtable resolvedSchemas, which may be null
+ * @throws Exception If there are problems with Java reflection (should
not happen)
+ */
+ static Map<String, Map<String, SoftReference<XmlSchema>>>
getResolvedSchemasHashtable(){
+ Map<String, Map<String, SoftReference<XmlSchema>>> ht = null;
+ try {
+ Field field =
SchemaBuilder.class.getDeclaredField("resolvedSchemas");
+ field.setAccessible(true);
+ ht = (Map<String, Map<String,
SoftReference<XmlSchema>>>)field.get(null);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ return ht;
+ }
+
+ /**
+ * Return the Hashtable for the current thread or null if there is not one.
+ * @return
+ */
+ static Map<String, SoftReference<XmlSchema>>
getThreadResolvedSchemaHashtable() {
+ Map<String, SoftReference<XmlSchema>> threadHashtable = null;
+ Map<String, Map<String, SoftReference<XmlSchema>>> ht =
getResolvedSchemasHashtable();
+ if (ht != null) {
+ String threadID = String.valueOf(Thread.currentThread().getId());
+ threadHashtable = (Map<String, SoftReference<XmlSchema>>)
ht.get(threadID);
+ }
+ return threadHashtable;
+ }
+
+ /**
+ * Set the resolvedSchemas collection to null. This should be done in a
finally block
+ * of any tests that cause an SchemaBuilder.initCache to be done to
cleanup for the next
+ * test.
+ */
+ static void resetResolvedSchemasHashtable() {
+ try {
+ Field field =
SchemaBuilder.class.getDeclaredField("resolvedSchemas");
+ field.setAccessible(true);
+ Map<String, Map<String, SoftReference<XmlSchema>>> ht =
(Map<String, Map<String, SoftReference<XmlSchema>>>) field.get(null);
+ if (ht != null) {
+ ht.clear();
+ field.set(null, null);
+ }
+ }catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ // Amount of time the testcase should wait on the test threads before
timing out
+ private static int THREAD_TIMEOUT = 90000;
+
+ /**
+ * Configure and start the test threads for the multi-threaded testing.
The threads will
+ * perform various tests between themselves such as clearing cache in one
thread and making
+ * sure the cache used by a different thread is not affected. A monitor
is used to control
+ * the synchonization between the threads and for communicating faliures
back to the test
+ * method.
+ *
+ * See thed Runnable classes for details on the tests performed.
+ *
+ * @param testMonitor Used to synchronize the tests between the threads
+ */
+ private void startupTestThreads(MultithreadUpdateLockMonitor testMonitor) {
+ TestingRunnable1 testRunnable1 = new TestingRunnable1();
+ testRunnable1.testMonitor = testMonitor;
+
+ TestingRunnable2 testRunnable2 = new TestingRunnable2();
+ testRunnable2.testMonitor = testMonitor;
+
+ TestingRunnable3 testRunnable3 = new TestingRunnable3();
+ testRunnable3.testMonitor = testMonitor;
+
+ Thread thread1 = new Thread(testRunnable1);
+ Thread thread2 = new Thread(testRunnable2);
+ Thread thread3 = new Thread(testRunnable3);
+
+ thread1.start();
+ thread2.start();
+ thread3.start();
+
+ // Join the threads to wait for their completion, specifying a timeout
to prevent
+ // a testcase hang if something goes wrong with the threads.
+ try {
+ thread1.join(THREAD_TIMEOUT);
+ thread2.join(THREAD_TIMEOUT);
+ thread3.join(THREAD_TIMEOUT);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ fail("Unable to join to testing threads");
+ }
+ }
+}
+
+/**
+ * Monitor used to control synchronization between the testing threads and
communicate failures
+ * back to the test method.
+ */
+class MultithreadUpdateLockMonitor {
+ boolean t1SetupComplete = false;
+ boolean t2SetupComplete = false;
+ Exception t1Exception = null;
+ Exception t2Exception = null;
+ Exception t3Exception = null;
+}
+//=================================================================================================
+// Test execution threads
+//=================================================================================================
+/**
+ * Thread 1 will do the following
+ * - Initialize the cache and verify it was used during a read
+ * - Unblock Thread 2
+ * - Wait until Thread 2 unblocks it
+ * - Verify that the clearCache done by Thread 2 did not affect this Thread's
cache.
+ */
+...@ignore
+class TestingRunnable1 implements Runnable {
+
+ MultithreadUpdateLockMonitor testMonitor = null;
+
+ public void run() {
+ SchemaBuilder.initCache();
+ Document doc = SchemaBuilderCacheTest.setupDocument();
+ XmlSchemaCollection schemaCol =
SchemaBuilderCacheTest.setupXmlSchemaCollection();
+ XmlSchema schema = schemaCol.read(doc, null);
+ if (schema == null) {
+ testMonitor.t1Exception = new Exception("Schema was null");
+ }
+
+ // If the cache is in use, it should not be null and there should be
an entry for this
+ // Thread
+ if (SchemaBuilderCacheTest.getResolvedSchemasHashtable() == null) {
+ testMonitor.t1Exception = new Exception("resolvedSchemas was
null");
+ }
+ Map<String, SoftReference<XmlSchema>> threadHT =
SchemaBuilderCacheTest.getThreadResolvedSchemaHashtable();
+ if (threadHT == null ) {
+ testMonitor.t1Exception = new Exception("Thread resolvedSchemas
was null");
+ }
+
+ if (threadHT.isEmpty()) {
+ testMonitor.t1Exception = new Exception("Thread resolvedSchemas
was empty");
+ }
+
+ synchronized(testMonitor) {
+ testMonitor.t1SetupComplete = true;
+ testMonitor.notifyAll();
+ while (!testMonitor.t2SetupComplete) {
+ try {
+ testMonitor.wait();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ testMonitor.t1Exception = new RuntimeException(e);
+ throw (RuntimeException) testMonitor.t1Exception;
+ }
+ }
+ }
+
+ // After the other thread does a reset, the cache for this thread
should NOT be null
+ if (SchemaBuilderCacheTest.getResolvedSchemasHashtable() == null) {
+ testMonitor.t1Exception = new Exception("resolvedSchemas was null
after reset");
+ }
+ threadHT = SchemaBuilderCacheTest.getThreadResolvedSchemaHashtable();
+ if (threadHT == null ) {
+ testMonitor.t1Exception = new Exception("Thread resolvedSchemas
was null after clear");
+ }
+
+ if (threadHT.isEmpty()) {
+ testMonitor.t1Exception = new Exception("Thread resolvedSchemas
was empty after clear");
+ }
+
+ // Issue our a clear on this TID, and now there should be no entires
for it
+ SchemaBuilder.clearCache();
+ if (SchemaBuilderCacheTest.getResolvedSchemasHashtable() == null) {
+ testMonitor.t1Exception = new Exception("resolvedSchemas was null
after clear on TID");
+ }
+
+ threadHT = SchemaBuilderCacheTest.getThreadResolvedSchemaHashtable();
+ if (threadHT != null ) {
+ testMonitor.t1Exception = new Exception("Thread resolvedSchemas
was not null after clear");
+ }
+ }
+}
+
+/**
+ * Thread 2 will:
+ * - Block until released by Thread 1
+ * - Initialize the cache then make sure it was used for a resolve on this
thread
+ * - clear the cache and make sure the entries for this Thread a removed
+ * - Unblock Thread 1 to make sure the clear did not affect it
+ */
+...@ignore
+class TestingRunnable2 implements Runnable {
+
+ MultithreadUpdateLockMonitor testMonitor = null;
+
+ public void run() {
+ synchronized (testMonitor) {
+ while (!testMonitor.t1SetupComplete) {
+ try {
+ testMonitor.wait();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ testMonitor.t2Exception = new RuntimeException(e);
+ throw (RuntimeException) testMonitor.t2Exception;
+ }
+ }
+ }
+ SchemaBuilder.initCache();
+ Document doc = SchemaBuilderCacheTest.setupDocument();
+ XmlSchemaCollection schemaCol =
SchemaBuilderCacheTest.setupXmlSchemaCollection();
+ XmlSchema schema = schemaCol.read(doc, null);
+ if (schema == null) {
+ testMonitor.t2Exception = new Exception("Schema was null");
+ }
+
+ // If the cache is in use, it should not be null.
+ if (SchemaBuilderCacheTest.getResolvedSchemasHashtable() == null) {
+ testMonitor.t2Exception = new Exception("resolvedSchemas was
null");
+ }
+ Map<String, SoftReference<XmlSchema>> threadHT =
SchemaBuilderCacheTest.getThreadResolvedSchemaHashtable();
+ if (threadHT == null ) {
+ testMonitor.t2Exception = new Exception("Thread resolvedSchemas
was null");
+ }
+ if (threadHT.isEmpty()) {
+ testMonitor.t2Exception = new Exception("Thread resolvedSchemas
was empty");
+ }
+
+ // Issue our a clear on this TID, and now there should be no entires
for it
+ SchemaBuilder.clearCache();
+ if (SchemaBuilderCacheTest.getResolvedSchemasHashtable() == null) {
+ testMonitor.t2Exception = new Exception("resolvedSchemas was null
after clear on TID");
+ }
+
+ threadHT = SchemaBuilderCacheTest.getThreadResolvedSchemaHashtable();
+ if (threadHT != null ) {
+ testMonitor.t2Exception = new Exception("Thread resolvedSchemas
was not null after clear");
+ }
+
+ synchronized (testMonitor) {
+ testMonitor.t2SetupComplete = true;
+ testMonitor.notifyAll();
+ }
+ }
+}
+
+/**
+ * Test that the init done by Thread 1 does NOT cause the cache to be used by
this thread
+ * which did not do an init.
+ */
+...@ignore
+class TestingRunnable3 implements Runnable {
+ MultithreadUpdateLockMonitor testMonitor = null;
+
+ public void run() {
+ synchronized (testMonitor) {
+ while (!testMonitor.t1SetupComplete) {
+ try {
+ testMonitor.wait();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ testMonitor.t3Exception = new RuntimeException(e);
+ throw (RuntimeException) testMonitor.t2Exception;
+ }
+ }
+ }
+ Document doc = SchemaBuilderCacheTest.setupDocument();
+ XmlSchemaCollection schemaCol =
SchemaBuilderCacheTest.setupXmlSchemaCollection();
+ XmlSchema schema = schemaCol.read(doc, null);
+ if (schema == null) {
+ testMonitor.t3Exception = new Exception("Schema was null");
+ }
+ Map<String, SoftReference<XmlSchema>> threadHT =
SchemaBuilderCacheTest.getThreadResolvedSchemaHashtable();
+ if (threadHT != null ) {
+ testMonitor.t3Exception = new Exception("Thread resolvedSchemas
was not null");
+ }
+
+
+ }
+
+}