Wednesday, September 22, 2010

Metawidget Neat Trick: Concurrent Inspectors

Here's another neat trick I've discovered while using Metawidget in my own work.

Metawidget comes with multiple Inspectors to inspect different aspects of your back-end architecture. It then merges the result. This merging is done by CompositeInspector which is itself just another Inspector and therefore pluggable. This means you can plug in different ways of running and merging multiple Inspectors.

What sorts of different ways? Well, since each Inspector is immutable, it's easy to isolate each one in its own Thread. And if you have some fancy processor with a bunch of cores (12, anyone?) you may as well put them to use!

Here's the code:
package com.myapp;

import java.util.concurrent.CyclicBarrier;

import org.metawidget.inspector.composite.*;
import org.metawidget.inspector.iface.*;
import org.w3c.dom.Document;

public class ConcurrentCompositeInspector
   extends CompositeInspector {

   private final Inspector[]   mConcurrentInspectors;

   public ConcurrentCompositeInspector( ConcurrentCompositeInspectorConfig config ) {

      super( config );

      Inspector[] concurrentInspectors = config.getConcurrentInspectors();

      // Must have at least one concurrentInspector (else we may as well use CompositeInspector)

      if ( concurrentInspectors == null || concurrentInspectors.length == 0 ) {
         throw InspectorException.newException( "ConcurrentCompositeInspector needs at least one concurrentInspector" );
      }

      // Defensive copy

      mConcurrentInspectors = new Inspector[concurrentInspectors.length];

      for ( int loop = 0, length = concurrentInspectors.length; loop < length; loop++ ) {
         Inspector inspector = concurrentInspectors[loop];

         for ( int checkDuplicates = 0; checkDuplicates < loop; checkDuplicates++ ) {
            if ( mConcurrentInspectors[checkDuplicates].equals( inspector ) ) {
               throw InspectorException.newException( "ConcurrentCompositeInspector's list of Concurrent Inspectors contains two of the same " + inspector.getClass().getName() );
            }
         }

         mConcurrentInspectors[loop] = inspector;
      }
   }

   /**
    * Overriden to use a CyclicBarrier to run inspectors concurrently.
    */

   @Override
   protected Document runInspectors( Document masterDocument, final Object toInspect, final String type, final String... names )
      throws Exception {

      // Run concurrent Inspectors...

      int length = mConcurrentInspectors.length;
      final Document[] concurrentDocuments = new Document[length];

      // Prepare a CyclicBarrier of 'n concurrent inspectors + 1 primary thread'

      final CyclicBarrier barrier = new CyclicBarrier( length + 1 );

      for ( int loop = 0; loop < length; loop++ ) {
         final int concurrentInspectorIndex = loop;

         new Thread( "ConcurrentCompositeInspector Thread" ) {

            @Override
            public void run() {

               try {
                  concurrentDocuments[concurrentInspectorIndex] = ConcurrentCompositeInspector.this.runInspector( mConcurrentInspectors[concurrentInspectorIndex], toInspect, type, names );
               } catch ( Exception e ) {
                  throw InspectorException.newException( e );
               } finally {
                  try {
                     barrier.await();
                  } catch ( Exception e ) {
                     throw InspectorException.newException( e );
                  }
               }
            }
         }.start();
      }

      // ...run regular Inspectors at the same time (if any)...

      Document masterDocumentToUse;

      try {
         masterDocumentToUse = super.runInspectors( masterDocument, toInspect, type, names );
      } finally {
         barrier.await();
      }

      // ...merge the result...

      for ( Document concurrentDocument : concurrentDocuments ) {
         masterDocumentToUse = combineInspectionResult( masterDocumentToUse, concurrentDocument );
      }

      // ...and return

      return masterDocumentToUse;
   }
}

You'll also need a Config class:
package com.myapp;

import org.metawidget.inspector.composite.CompositeInspectorConfig;
import org.metawidget.inspector.iface.Inspector;
import org.metawidget.util.simple.ObjectUtils;

public class ConcurrentCompositeInspectorConfig
   extends CompositeInspectorConfig {

   private Inspector[]   mConcurrentInspectors;

   public Inspector[] getConcurrentInspectors() {

      return mConcurrentInspectors;
   }

   public ConcurrentCompositeInspectorConfig setConcurrentInspectors( Inspector... concurrentInspectors ) {

      mConcurrentInspectors = concurrentInspectors;

      return this;
   }

   @Override
   public boolean equals( Object that ) {

      if ( this == that ) {
         return true;
      }

      if ( that == null ) {
         return false;
      }

      if ( getClass() != that.getClass() ) {
         return false;
      }

      if ( !ObjectUtils.nullSafeEquals( mConcurrentInspectors, ( (ConcurrentCompositeInspectorConfig) that ).mConcurrentInspectors ) ) {
         return false;
      }

      return super.equals( that );
   }

   @Override
   public int hashCode() {

      int hashCode = super.hashCode();
      hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode( mConcurrentInspectors );

      return hashCode;
   }
}

And then you can deploy it like this:
<inspector>
   <concurrentCompositeInspector xmlns="java:com.myapp"
      config="ConcurrentCompositeInspectorConfig">
      <inspectors>
         <array>
            ...
         </array>
      </inspectors>
      <concurrentInspectors>
         <array>
            ...
         </array>
      </concurrentInspectors>
   </concurrentCompositeInspector>
</inspector>

This should spread the load across your cores. It's not a no-brainer: some Inspectors may rely on ThreadLocal variables (like FacesContext) and so will need to be kept on the primary Thread. But where it can be used, it works well.

Hope that helps!

0 comments: