Saturday, June 6, 2009

Google App Engine Wicket ModificationWatcher

What is the ModificationWatcher?
ModificationWatcher is a Wicket class that checks to see if resources have been modified and reloads them without restarting the servlet container (where possible). This is made possible by setting up a polling frequency in the init() method of the WebApplication class for your project. Unfortunately, Google App Engine does not allow the spawning of new threads (on which the ModificationWatcher class depends on), hence a work-around is necessary. First we need to look at the WicketApplication class, as shown in the example below:

Example WicketApplication Class
view plaincopy to clipboardprint?
  1. import org.apache.wicket.Application;
  2. import org.apache.wicket.protocol.http.HttpSessionStore;
  3. import org.apache.wicket.protocol.http.WebApplication;
  4. import org.apache.wicket.protocol.http.WebRequest;
  5. import org.apache.wicket.session.ISessionStore;
  6. import org.apache.wicket.util.lang.PackageName;
  7. public class WicketApplication extends WebApplication {
  8. @Override
  9. protected void init() {
  10. super.init();
  11. // getResourceSettings().setResourcePollFrequency(null);
  12. }
  13. protected WebRequest newWebRequest(HttpServletRequest servletRequest) {
  14. getResourceSettings().getResourceWatcher(true).start(
  15. getResourceSettings().getResourcePollFrequency());
  16. return super.newWebRequest(servletRequest);
  17. }
  18. @Override
  19. public String getConfigurationType() {
  20. return Application.DEVELOPMENT;
  21. // return Application.DEPLOYMENT;
  22. }
  23. @Override
  24. protected ISessionStore newSessionStore() {
  25. return new HttpSessionStore(this);
  26. // return new SecondLevelCacheSessionStore(this, new DiskPageStore());
  27. }
  28. }

The first 'hack' to getting Wicket working in GAE is to set the session store type for Wicket to HttpSessionStore, instead of using the SecondLevelCacheSessionStore as the latter directly refers to the file system and is not allowed in the app engine sandbox. This is done by overriding the newSessionStore() method [line 29 in the code extract shown above].

Now, back to the ModificationWatcher. If you want to completely disable it, you can set the poll frequency to null in the init method of your WebApplication class [this has been commented out in line 13 above]. However, if you are in development, this will mean that any changes to the source pages [in my experience its really been the HTML pages], you will have to restart your servlet container to pick-up the changes, which is not very efficient for development purposes. This work-around by-passes the polled watching for file changes and checks to see if the file has changed every time it is requested for. This is achieved by overriding the newWebRequest() method [see line 16 above]. This is fine for development purposes, and MUST be disabled for production, for obvious reasons. 

And for this to work, you need a version of the ModificationWatcher class that does not spawn any threads. ModificationWatcher was declared a final class and hence cannot be extended, and so you need to override the Wicket provided version with one of your own. This is easily achieved by creating a similar package structure in your source code and creating the file : org.apache.wicket.util.watch.ModificationWatcher. The code for this class is as below:


  1. /**
  2. * Monitors one or more <code>IModifiable</code> objects, calling a
  3. * {@link IChangeListener IChangeListener} when a given object's modification time changes.
  4. *
  5. * @author Jonathan Locke
  6. * @since 1.2.6
  7. */
  8. public final class ModificationWatcher
  9. {
  10. /** logger */
  11. private static final Logger log = LoggerFactory.getLogger(ModificationWatcher.class);
  12. /** maps <code>IModifiable</code> objects to <code>Entry</code> objects */
  13. private final Map modifiableToEntry = new ConcurrentHashMap();
  14. /** the <code>Task</code> to run */
  15. //private Task task;
  16. /**
  17. * Container class for holding modifiable entries to watch.
  18. */
  19. private static final class Entry
  20. {
  21. // The most recent lastModificationTime polled on the object
  22. Time lastModifiedTime;
  23. // The set of listeners to call when the modifiable changes
  24. final ChangeListenerSet listeners = new ChangeListenerSet();
  25. // The modifiable thing
  26. IModifiable modifiable;
  27. }
  28. /**
  29. * Default constructor for two-phase construction.
  30. */
  31. public ModificationWatcher()
  32. {
  33. }
  34. /**
  35. * Constructor that accepts a <code>Duration</code> argument representing the poll frequency.
  36. *
  37. * @param pollFrequency
  38. * how often to check on <code>IModifiable</code>s
  39. */
  40. public ModificationWatcher(final Duration pollFrequency)
  41. {
  42. start(pollFrequency);
  43. }
  44. /**
  45. * Adds an <code>IModifiable</code> object and an <code>IChangeListener</code> object to
  46. * call when the modifiable object is modified.
  47. *
  48. * @param modifiable
  49. * an <code>IModifiable</code> object to monitor
  50. * @param listener
  51. * an <code>IChangeListener</code> to call if the <code>IModifiable</code> object
  52. * is modified
  53. * @return <code>true</code> if the set did not already contain the specified element
  54. */
  55. public final boolean add(final IModifiable modifiable, final IChangeListener listener)
  56. {
  57. // Look up entry for modifiable
  58. final Entry entry = (Entry)modifiableToEntry.get(modifiable);
  59. // Found it?
  60. if (entry == null)
  61. {
  62. if (modifiable.lastModifiedTime() != null)
  63. {
  64. // Construct new entry
  65. final Entry newEntry = new Entry();
  66. newEntry.modifiable = modifiable;
  67. newEntry.lastModifiedTime = modifiable.lastModifiedTime();
  68. newEntry.listeners.add(listener);
  69. // Put in map
  70. modifiableToEntry.put(modifiable, newEntry);
  71. }
  72. else
  73. {
  74. // The IModifiable is not returning a valid lastModifiedTime
  75. log.info("Cannot track modifications to resource " + modifiable);
  76. }
  77. return true;
  78. }
  79. else
  80. {
  81. // Add listener to existing entry
  82. return entry.listeners.add(listener);
  83. }
  84. }
  85. /**
  86. * Removes all entries associated with an <code>IModifiable</code> object.
  87. *
  88. * @param modifiable
  89. * an <code>IModifiable</code> object
  90. * @return the <code>IModifiable</code> object that was removed, else <code>null</code>
  91. */
  92. public IModifiable remove(final IModifiable modifiable)
  93. {
  94. final Entry entry = (Entry)modifiableToEntry.remove(modifiable);
  95. if (entry != null)
  96. {
  97. return entry.modifiable;
  98. }
  99. return null;
  100. }
  101. /**
  102. * Starts watching at a given <code>Duration</code> polling rate.
  103. *
  104. * @param pollFrequency
  105. * the polling rate <code>Duration</code>
  106. */
  107. public void start(final Duration pollFrequency)
  108. {
  109. // Construct task with the given polling frequency
  110. //task = new Task("ModificationWatcher");
  111. //task.run(pollFrequency, new ICode()
  112. //{
  113. // public void run(final Logger log)
  114. // {
  115. // Iterate over a copy of the list of entries to avoid
  116. // concurrent
  117. // modification problems without the associated liveness issues
  118. // of holding a lock while potentially polling file times!
  119. for (final Iterator iterator = new ArrayList(modifiableToEntry.values()).iterator(); iterator
  120. .hasNext();)
  121. {
  122. // Get next entry
  123. final Entry entry = (Entry)iterator.next();
  124. // If the modifiable has been modified after the last known
  125. // modification time
  126. final Time modifiableLastModified = entry.modifiable.lastModifiedTime();
  127. if (modifiableLastModified.after(entry.lastModifiedTime))
  128. {
  129. // Notify all listeners that the modifiable was modified
  130. entry.listeners.notifyListeners();
  131. // Update timestamp
  132. entry.lastModifiedTime = modifiableLastModified;
  133. }
  134. }
  135. // }
  136. //});
  137. }
  138. /**
  139. * Stops this <code>ModificationWatcher</code>.
  140. */
  141. public void destroy()
  142. {
  143. //if (task != null)
  144. //{
  145. // task.stop();
  146. // task.interrupt();
  147. //}
  148. }
  149. /**
  150. * Retrieves a key set of all <code>IModifiable</code> objects currently being monitored.
  151. *
  152. * @return a <code>Set</code> of all <code>IModifiable</code> entries currently maintained
  153. */
  154. public final Set getEntries()
  155. {
  156. return modifiableToEntry.keySet();
  157. }
  158. }

All that has been done is that the Threading references have been taken out so no new threads are spawned and the check for the resources is done in the same thread as that the one that is servicing the user request. This is not the most elegant solution, but it works.

This hack is based on the following sources: 

1 comment:

Unknown said...

The resource modification watching will be fixed as of Wicket 1.4.0-RC6. The Wicket guys introduced a replaceable IModificationWatcher interface.

https://issues.apache.org/jira/browse/WICKET-2340