spring thread scope and test failures

I needed a working “thread” scope for one of my spring beans, and surprisingly this cost me a bit more effort than anticipated.

Let’s start with my initials attempt, which was able to withstand some testcases.

/**
 * Thread scope which allows putting data in thread scope and clearing up afterwards.
 *
 * @author Joachim Van der Auwera
 */
@Component
public class ThreadScope implements Scope {

  /**
   * Get bean for given name in the "thread" scope.
   *
   * @param name name of bean
   * @param factory factory for new instances
   * @return bean for this scope
   */
  public Object get(String name, ObjectFactory<?> factory) {
    ThreadScopeContext context = ThreadScopeContextHolder.getContext();

    Object result = context.getBean(name);
    if (null == result) {
      result = factory.getObject();
      context.setBean(name, result);
    }
    return result;
  }

  /**
   * Removes bean from scope.
   *
   * @param name bean name
   * @return previous value
   */
  public Object remove(String name) {
    ThreadScopeContext context = ThreadScopeContextHolder.getContext();
    return context.remove(name);
  }

  public void registerDestructionCallback(String name, Runnable callback) {
    ThreadScopeContextHolder.getContext().registerDestructionCallback(name, callback);
  }

  /**
   * Resolve the contextual object for the given key, if any. E.g. the HttpServletRequest object for key "request".
   *
   * @param key key
   * @return contextual object
   */
  public Object resolveContextualObject(String key) {
    return null;
  }

  /**
   * Return the conversation ID for the current underlying scope, if any.
   * <p/>
   * In this case, it returns the thread name.
   *
   * @return thread name as conversation id
   */
  public String getConversationId() {
    return Thread.currentThread().getName();
  }

}

public final class ThreadScopeContextHolder {

  private static final ThreadLocal<ThreadScopeContext> CONTEXT_HOLDER =
      new ThreadLocal<ThreadScopeContext>() {
        protected ThreadScopeContext initialValue() {
          return new ThreadScopeContext();
        }
      };

  private ThreadScopeContextHolder() {
    // utility object, not allowed to create instances
  }

  /**
   * Get the thread specific context.
   *
   * @return thread scoped context
   */
  public static ThreadScopeContext getContext() {
    return CONTEXT_HOLDER.get();
  }

  /**
   * Set the thread specific context.
   *
   * @param context thread scoped context
   */
  public static void setContext(ThreadScopeContext context) {
    ThreadScopeContextHolder.CONTEXT_HOLDER.set(context);
  }
}

public class ThreadScopeContext {

  protected final Map<String, Bean> beans = new HashMap<String, Bean>();

  /**
   * Get a bean value from the context.
   *
   * @param name bean name
   * @return bean value or null
   */
  public Object getBean(String name) {
    Bean bean = beans.get(name);
    if (null == bean) {
      return null;
    }
    return bean.object;
  }

  /**
   * Set a bean in the context.
   *
   * @param name bean name
   * @param object bean value
   */
  public void setBean(String name, Object object) {
    Bean bean = beans.get(name);
    if (null == bean) {
      bean = new Bean();
      beans.put(name, bean);
    }
    bean.object = object;
  }

  /**
   * Remove a bean from the context, calling the destruction callback if any.
   *
   * @param name bean name
   * @return previous value
   */
  public Object remove(String name) {
    Bean bean = beans.get(name);
    if (null != bean) {
      beans.remove(name);
      bean.destructionCallback.run();
      return bean.object;
    }
    return null;
  }

  /**
   * Register the given callback as to be executed after request completion.
   *
   * @param name The name of the bean.
   * @param callback The callback of the bean to be executed for destruction.
   */
  public void registerDestructionCallback(String name, Runnable callback) {
    Bean bean = beans.get(name);
    if (null == bean) {
      bean = new Bean();
      beans.put(name, bean);
    }
    bean.destructionCallback = callback;
  }

  /** Clear all beans and call the destruction callback. */
  public void clear() {
    for (Bean bean : beans.values()) {
      if (null != bean.destructionCallback) {
        bean.destructionCallback.run();
      }
    }
    beans.clear();
  }

  /** Private class storing bean name and destructor callback. */
  private class Bean {

    private Object object;
    private Runnable destructionCallback;

    public Object getObject() {
      return object;
    }

    public void setObject(Object object) {
      this.object = object;
    }

    public Runnable getDestructionCallback() {
      return destructionCallback;
    }

    public void setDestructionCallback(Runnable destructionCallback) {
      this.destructionCallback = destructionCallback;
    }
  }
}

You need the following excerpt in your applicationContext to make this work.

<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
  <property name="scopes">
    <map>
      <entry key="thread">
        <bean class="my.package.ThreadScope"/>
      </entry>
    </map>
  </property>
</bean>

Your bean which needs this “thread” context can be declared using the following annotation (you need the proxy mode to assure the thread specific instance is always used :

@Component
@Scope(value = "thread", proxyMode = ScopedProxyMode.TARGET_CLASS)

This seemed to work flawlessly in most cases. However on one machine we had failing unit tests, while on a few other systems, it all worked without problems.

After a lot of hunting, the culprit was found.

For starters I have to clarify that we use junit 4.5 for testing and with the test runner. So the test classes are annotated like this

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { ... })

to allow autowiring etc in the class.

The test runner is smart and caches the application context. The context configuration files are only read when the locations change, not for each test method and not even for each test class. In case that does not work for you, you can always annotate your tests with @DirtiesContext to assure a refresh of the application context after the test method had completed.

Back to our problem, the thread scoped beans could contain references to services with different content than expected. This is caused by having an instance of the bean which was initialised for a different application context, but when the application context is refreshed, the thread local instances are not cleared.

The solution is to assure the thread local variables are cleared when needed. For this, the ThreadScopeContextHolder is modified by adding the following method :

public static void clear() {
  CONTEXT_HOLDER =
    new ThreadLocal<ThreadScopeContext>() {
      protected ThreadScopeContext initialValue() {
        return new ThreadScopeContext();
      }
    };
}

This is then called from the ThreadScope class by enabling the “destroy” hook.

@Component
public class ThreadScope implements Scope, DisposableBean {

  ...

  public void destroy() throws Exception {
    ThreadScopeContextHolder.clear();
  }
}

This fixed most of the failures, but not all.
The destroy callback is called when the application context is refreshed because of the presence of the ” @DirtiesContext” annotation, but not when the application context changes because a test is started which requires a different “@ContextConfiguration”.

To fix that the ThreadScope class also clears the thread locals when created.

public ThreadScope() {
  ThreadScopeContextHolder.clear();
}

2 Comments

  1. joachim says:

    Actually this does not fix all. The caching assure that the thread scoped stuff can be from a different context. If three tests run after each other and they use context definition A,B,A then the third test may have thread scoped values from the second test.

    You have to make sure these are cleaned up an @After annotated block.

  2. Bethel Ewen says:

    Nicely put. Kudos!.
    Helpful facts, Thank you.

Leave a Reply

Your email address will not be published. Required fields are marked *

question razz sad evil exclaim smile redface biggrin surprised eek confused cool lol mad twisted rolleyes wink idea arrow neutral cry mrgreen

*