Dynamic App Runtime Updates Using Mule Punching
Mule punching is a technique for extending or modifying the run-time code in Mule without stopping the server. The term is based on the Ruby programming jargon duck punching, where if duck typing doesn't work, you punch the duck until it does. If the services app doesn't do what you need, you can punch the mule until it does by changing the methods and attributes of service components and transformers without stopping the server, and even reconfigure the behavior of third-party packages without access to their code... all while the Mule server runs.
My team is building an enterprise-class system that has components that run on Mule and in a distributed environment across hundreds (or even thousands) of systems like in diagram 1. We selected a Java/ESB for the data center portions of the project, and Python for the distributed parts, both backed by mongoDB, arguably the most powerful and flexible NoSQL database today. As we got down to business, we realized that several data access and business logic functions had to be replicated in Java and Python.
This doubled the development and testing effort and we began exploring how to leverage JSR-223. We chose the Mule Integration Platform as our core application container but, powerful as it is, development and testing are very time consuming because of the compilation/testing/deployment/testing cycles in Mule. What if we could write only a single set of components in Python, and run them across all environments? What if we could reduce how much time we screw with Maven, Eclipse, Hudson, and Mule during each development integration cycle?
Motivation
- Single code library for use in the distributed and data center environments – develop and test only once for identical functions
- Use of a modern programming language with an elegant syntax, a rich class library and with a broad installed base (in scripting terms)
- Ability to modify code at runtime without having to stop Mule (or the optional app server hosting Mule) to change the application's behavior
- Code deployments without bundles or OSGi, since Mule doesn't support either yet, in A/A clustered environments - experimental right now, but we can probably get there soon
- Reduction or elimination of the compilation/testing/deployment cycles
What We Used
- Deployed on both Mule 2.2.1 Community Edition or Mule 2.2.4 Enterprise Edition
- Jython 2.5.1 - Python running on the JVM that can use its standard library and whatever Java APIs are available on the same JVM
- mongoDB - a NoSQL, document-oriented database with rich Python and Java APIs
Installation
Install Mule as recommended, set the the $MULE_HOME and $MULE_LIB environment as indicated. This will be important later. The rest of the components' installation isn't complicated but does require that you follow these instructions exactly or you'll spend a lot of time troubleshooting why things don't work as expected.
$MULE_HOME
points at /home/appserver/mule
in our environment.
Installing Jython
Download Jython 2.5.2 when it becomes available; we used Jython 2.5.1 trunk because the stable release has a bug in the Scripting Engine that prevents Mule from binding its environment to the Python components. You need the sources for building the Jython 2.5.1 environment and its installer:
svn co https://jython.svn.sourceforge.net/svnroot/jython/trunk/jython svn co https://jython.svn.sourceforge.net/svnroot/jython/trunk/installer cd jython ant clean ; ant install
Run the installer (see figure 1); it's very important that you generate the standalone jython.jar file because it contains every Python and Java class for your Mule environment; if you end up building any of the other runtimes you'll be haunted by frustration and misery every time that you try to run your scripts or import a module, whether from Java or from Python. You've been warned! So, do this:
java -jar jython_installer-2.5.1.jar
- When prompted, select: Standalone (a callable .jar file)
-
Select
$MULE_HOME/lib/user
as the installation target; it was/home/appserver/mule/lib/user
- Accept the prompts until you click on the Finish button
Ensure that everything is working by writing a simple greeting program for Jython to run:
echo ‘print "Hello Pythonic world!"' | java -jar $MULE_HOME/lib/user/jython.jar
If all goes well, your program will greet you back after initializing its own environment. You're ready to rock now!
Running Python Code in Mule
MuleSoft documents how to run scripts within Mule and uses Groovy for its examples. Here is the Python equivalent to their sample script with some logging thrown in:
<script:component> <script:script engine='jython'> response = 'Payload = '+payload log.info(response) result = response </script:script> </script:component>
Yes, Python's indentation rules apply even if the script is embedded in a Mule configuration file. It's a better idea to build the script and either put it in a .jar file or in the file system (more on that later), and just define a script component in the config file:
<script:script name='Greeting' engine='jython' file='/home/appserver/mule/lib/user/greeting.py' />
There are two possible ways of referencing the script in the file attribute:
- Provide the full path to the script, or its path relative to Mule's home directory
- Provide its relative path from the .jar file's root containing the script; this .jar will life in
$MULE_HOME/lib/user
like any other end-user, application package
Mule provides result and log objects via context binding, along with other useful references to things like the MuleContext, raw message, and any message properties.
Defining Robust Python Services for Mule
Though scripting can be used for quick development of simple components, we have a requirement to make robust Python components that will work in the Mule container as well as in a distributed environment that includes Windows, AIX, Linux, OS X, and Solaris. Thus we laid the code out in the same way that we'd arrange our Java code: service components have an entry point that takes the message request and returns some object as a response. One other requirement for us is to keep the Mule and any other Java code isolated from the rest of the Python code because of reusability. As such, the code is laid out the three distinct layers shown in diagram 2:
- A simple script file that provides the entry point for Mule to call the business logic
- A Mule component that combines any calls to Java and Python code that are specific to the Mule environment
- Any number of business logic components, written in 100% Pure Python for maximum portability
In addition to this, the Mule component should get references to objects like the loggers or Mule entities only through the Mule script bindings to avoid or minimize class loader conflicts in the scripted components.
A Sample Application
Listing 1 - mulescript.py shows a basic script's entry point. This file is loaded when the script is first invoked via an endpoint. All it needs to do is import the Mule component that will perform the actual work, create an instance, and execute a service request, like in mule-punch-config.xml.
The Mule component in listing 2 - the muleservice.py module is similar to a restlet (see the Mule RESTPack Restlet interface for details). This component handles only GET requests and creates an instance of BusinessObject from listing 3 - businessmodule.py to service the request. This module is the "glue" that combines Java and Python code calls specific to the Mule environment. By confining them to this module we get the best of both worlds: a single place where portability issues may arise and where we can control the interface with the Java environment, and the ability to change the code dynamically that affects this and the business objects layer.
The component needs to behave like a full-blown Mule entity, which normally responds with an instance of a MuleMessage. The code in lines 44 and 45 shows how a new DefaultMuleMessage is created and set with the object's response and optional HTTP code (200 or 400, depending on how the request was handled). The Python and Java code are interacting without issues in this layer, even though the original response is a string generated from Python. Mixing and matching is easy.
The classes and objects, Java or otherwise, are imported only once when the script
first loads, unless the scripting engine is told to reload. Just add a call to reload(module)
for every module or script that you want to be able to change/update dynamically
without having to stop the Mule server.
Mule Punching Your Code
One of the most annoying things about Mule development is dealing with build/test/package/stopMule/deploy/startMule/test cycle. The ability to deploy code dynamically may be valuable in a production server as well. Imagine, for example, that the code in the previous example needs to return a JSON object instead of a plain string. In following SOA/Mule best practices, the business objects should deal with native objects, whether Java or Python. Either the service component or a transformer should modify those business objects for presentment to conform to a given transport or protocol.
Don't stop your Mule server. Start your favorite programming editor and open the file
$MULE_HOME/lib/user/article/muleservice.py
you're about to mule punch JSON support
into the service's response code without stopping Mule!
JSON support is native to Python version 2.6 - unfortunately, Jython is based on 2.5. Fortunately, support is based on a de facto standard defined in the simplejson module. As a developer you have the option of making up for this shortcoming in Jython in either of these two ways:
- Download simplejson and put it somewhere in the class path/sys.path like you did with the code in this examples, either as source or as a .jar
- Use some Java library, like Gson, which you may already have in use elsewhere in your Mule configuration
Let's add simplejson support first. Assuming that you downloaded and installed the module and it's in your class path/sys.path, make these changes to muleservice.py:
- Add:
import simplejson as json
- Add:
self.response = json.dumps({ 'nStatus' : nStatus, 'response' : self.response})
before building the Mule message.
Save the file, and hit the service endpoint again - this time you will get either of these responses:
{"nStatus": 200, "response": "2010-02-22"} {"nStatus": 400, "response": "Invalid HTTP method called!"}
Pretty nifty, isn't it?
Using Mule/Spring Components
Imagine that you don't want to add more code to the server since other
components in your infrastructure already use the Gson Java package.
Ensure that gson-1.3.jar (or a more current version) exists in
$MULE_HOME/lib/user
so that Mule can find it, then make
these changes WITHOUT STOPPING MULE:
- Add:
import com.google.gson as gson
- Add:
from gson import Gson
- Add this class variable and create a new instance of the Gson object:
converter = Gson()
Try testing your endpoint. You will get this error:
ImportError: No module named gson
Even though the Gson module is available in your server it may not have been used yet by any Java or Python component; the Spring container hasn't loaded the .jar, and thus it's invisible to your script. Let's make it available to Spring by adding this line to the mule-punch-config.xml file:
<spring:bean id='GsonCache' class='com.google.gson.Gson'/>
Stop Mule to reload the configuration file. Ensure that any Mule/Spring/application
components are loaded by Mule's Spring container during server initialization so
that they are visible in the scripts. Also, ensure that any .jar files required in
your script are stored in $MULE_HOME/lib/user
, not in $MULE_HOME/lib/opt
, or a
different class loader will handle them and they won't be available to the script.
Start Mule, and test the endpoint. This time you'll get the same response, without errors. We still have simplejson and now Gson in the same script, though, so let's remove simplejson from muleservice.py without stopping the server:
- Remove:
import simplejson as json
- Change the line with:
json.dumps(...)
toself.response = converter.toJson({ 'nStatus' : nStatus, 'response' : self.response, ‘encoder' : ‘Gson'})
Test your endpoint again. This time you'll get the response:
{"nStatus":200,"response":"2010-02-22","encoder":"Gson"}
The toJson()
Java method is catching a Python dictionary and operating on it
without issues. Jython will map the objects to the appropriate Java counterparts
without user intervention. APIs on both Python and Java become available to
the other without fuss.
You can continue to modify the code as described in the previous sections. You can now perform dynamic Mule component updates!
File System And Editor Lazy Commits
Some editors and some file systems don't commit the file instantly to the file system. OS X HFS+, for example, has a "lazy fsync()" that results in a slight delay when saving a file on a busy system. Some editors make a backup before committing the file to disk. Either way you may see an occasional one- to two-second delay between saving the file and being able to see the update on the Mule server. Check your editor's and file system's tech notes to see how to get around this. On Linux, using ext3 and Vim with full sync() the delay is almost non-existent. On Samba-mounted drives it can take several seconds to update the file.
Conclusion
Mule punching is a great technique for dynamic Mule code updates at runtime that can help expedite the development process while leveraging the combined APIs from a scripting language and Java. More research is needed but a refinement to this technique could be used for dynamic updates in production servers as a stopgap measure until a clean dynamic loading mechanism like OSGi is available for Mule servers. Faster development, seamless updates, and portable integration... What's not to like?
Acknowledgements
Special thanks to Alex Grönholm, sabi, pjenvey, and the rest of the team in the #jython IRC channel on freenode for their help with solving problems during our R&D phase.
Scalable Systems Newsletter
Subscribe to the newsletter and get every issue mailed free - with access to the latest system scalability, high availability, and performance news.