Adding caching to Spring Python objects

In this example, we will enhance the wiki engine that we have been developing by writing a service that retrieves wiki text from the database and converts it to HTML. Then, to limit the load on our infrastructure, we will add caching support.

  1. First, we need to code our service. The service will call a data access component to retrieve the wiki text stored in our database. Then we will convert it to HTML and hand it back to the caller.
    class WikiService(object):
    def __init__(self, data_access):
    self.data_access = data_access
    def get_article(self, article):
    return self.data_access.retrieve_wiki_text(article)
    def store_article(self, article):
    self.data_access.store_wiki_text(article)
    def html(self, text):
    pass # return wiki text converted to HTML
    def statistics(self, article):
    hits = self.data_access.hits(article)
    return (hits, hits /
    len(self.data_access.edits(article)))
    

    Here we have a WikiService similar to the one we used in our IoC example. In addition to having a statistics method, this version is able to retrieve wiki text from the database and format it into HTML. It can also store edited wiki articles in the database. In this example we aren't particularly concerned with the code that transforms wiki text to HTML and so we've omitted it.

  2. Let's put our WikiService in a Spring Python IoC container, so that it can be used as part of our wiki application.
    from springpython.config import PythonConfig
    from springpython.config import Object
    class WikiProductionAppConfig(PythonConfig):
    def __init__(self):
    super(WikiProductionAppConfig, self).__init__()
    @Object
    def data_access(self):
    return MysqlDataAccess()
    @Object
    def wiki_service(self):
    return WikiService(self.data_access())
    

    This should look familiar. It is the same IoC container definition we used in the previous chapter. The following block diagram shows the components of our application, with the Wiki's view layer calling into WikiService, which in turn calls data_access, finally reaching the database.

    Adding caching to Spring Python objects
  3. Let's assume that our wiki engine is now being used for a very popular site with lots of articles and users. Multiple requests for the same article between edits are likely going to be common and will tax the database server with a database query every time an article is retrieved. To limit that performance hit, let's code a simple caching solution.
    class WikiServiceWithCaching(object):
    def __init__(self, data_access):
    self.data_access = data_access
    self.cache = {}
    def get_article(self, article):
    if article not in self.cache:
    self.cache[article] =
    self.data_access.retrieve_wiki_text(article)
    return self.cache[article]
    def store_article(self, article):
    del self.cache[article]
    self.data_access.store_wiki_text(article)
    def html(self, text):
    pass # return wiki text converted to HTML
    def statistics(self, article):
    hits = self.data_access.hits(article)
    return (hits, hits /
    len(self.data_access.edits(article)))
    

    To handle multiple requests for the same article, WikiServiceWithCaching stores the wiki text in an internal dictionary called cache. This cache is then checked for the requested article before falling back to the database if the article is not in the cache. The cache is cleared whenever new edits are made.Finally the statistics don't have any caching at all.

    To bring in our new caching wiki service, we just need to change the wiki_service declaration in our IoC container definition.

    class WikiProductionAppConfig(PythonConfig):
    def __init__(self):
    super(WikiProductionAppConfig, self).__init__()
    @Object
    def data_access(self):
    return MysqlDataAccess()
    @Object
    def wiki_service(self):
    return WikiServiceWithCaching(self.data_access())
    

    This idea is pretty simple and should lessen the load on our database server. However, by mixing caching with wiki services we have violated the Single Responsibility Principle (SRP): a class should have one (and only one), reason to change. Changes to one of these functions could break the other. It is also harder to isolate these functions for testing and, ultimately, makes the code harder to read and understand.

  4. Let's decouple these two concerns, maintaining the wiki and caching, by pulling our caching mechanism into a separate class and then delegating to our original WikiService.
    class CachedService(object):
    def __init__(self, delegate):
    self.cache = {}
    self.delegate = delegate
    def get_article(self, article):
    if article in self.cache:
    return self.cache[article]
    else:
    return self.delegate.get_article(article)
    def store_article(self, article):
    del self.cache[article]
    self.delegate.store_article(article)
    def html(self, text):
    return self.delegate.html(text)
    def statistics(self, article):
    return self.delegate.statistics(article)
    
  5. Now we need to update our IoC configuration.
    class WikiProductionAppConfig(PythonConfig):
    def __init__(self):
    super(WikiProductionAppConfig, self).__init__()
    @Object
    def data_access(self):
    return MysqlDataAccess()
    @Object
    def wiki_service(self):
    return CachedService(WikiService(self.data_access()))
    

    Our architecture has changed slightly, with the Wiki view layer calling into our CachedService.

    Adding caching to Spring Python objects

    Now the SRP is obeyed as each class has only one responsibility. One class handles the caching by passing off all necessary WikiService calls to delegate. However, it isn't very practical. While CachedService (Code in Text) separates caching logic from WikiService, our usage of an adapter introduces some ugly constraints. The CachedService must have the same interface as the WikiService in order to handle its requests and we even had to code a passthrough for statistics. CachedService is also too specialized and wouldn't work for any generalized solution. Every time we add another method to WikiService, we have to change CachedService, giving us a tightly coupled pair of classes. Let's fix that now.

  6. Let's use Spring Python to code an Interceptor that not only separates caching from wiki text management, but is also general enough to be reused in other places.
    from springpython.aop import *
    class CachingInterceptor(MethodInterceptor):
    def __init__(self):
    self.cache = {}
    def invoke(self, invocation):
    if invocation.method_name.startswith("get"):
    if invocation.args not in self.cache:
    self.cache[invocation.args] =
    invocation.proceed()
    return self.cache[invocation.args]
    elif invocation.method_name.startswith("store"):
    del self.cache[invocation.args]
    invocation.proceed()
    

    Note

    CachingInterceptor defines a Spring Python aspect that has all the caching functionality and none of the wiki functionality. It contains both Advice (the caching functionality) and a pointcut (only applies to methods starting with get).

  7. To use CachingInterceptor, we must create a Spring Python AOP proxy with an instance of our aspect as well as an instance of WikiService.
    class WikiProductionAppConfig(PythonConfig):
    def __init__(self):
    super(WikiProductionAppConfig, self).__init__()
    @Object
    def data_access(self):
    return MysqlDataAccess()
    @Object
    def wiki_service(self):
    return ProxyFactoryObject(
    target=WikiService(self.data_access()),
    interceptors=CachingInterceptor())
    

    By creating a proxy and wiring it with our aspect, we have dynamically woven the aspect into the code. In our situation, we have created an instance of Spring Python's ProxyFactoryObject to combine WikiService with CachingInterceptor.

    By moving the Spring Python object wiki_service from WikiService to the ProxyFactoryObject, our call sequence now fl ows through our new interceptor, as shown in the following diagram.

    Adding caching to Spring Python objects

    Now, instead of having a specialized cache handler, we have a proxy that links to our generic caching aspect. Whenever the view layer submits a request to wiki_service, the calls get routed to CachingInterceptor.

    The key difference between CachingInterceptor and the earlier CachedService is how Spring Python bundles up all the information of the original method call into the invocation argument and dispatches it to the invoke method of CachingInterceptor. This allows us to manage entering and exiting from any method on WikiService in one place. This behavior is known as advising the target.

    In our example, CachingInterceptor checks if the target method name starts with get. If so, it checks the cache, using the target method's arguments as the key (in our case, the article name). If the arguments are not found in the cache, CachingInterceptor calls WikiService through invocation.proceed() as if we had called it directly. The results are stored in the cache, and then returned to the view layer. If the target method name starts with store, the cache entry is deleted followed by invocation.proceed().

  8. ProxyFactoryObject routes all method requests through our aspect. However, CachingInterceptor can't handle a call to statistics, because it doesn't start with get or store. To deal with this, let's insert another piece of advice that only sends get and store calls to CachingInterceptor, and all others directly to WikiService.
    class WikiProductionAppConfig(PythonConfig):
    def __init__(self):
    super(WikiProductionAppConfig, self).__init__()
    @Object
    def data_access(self):
    return MysqlDataAccess()
    @Object
    def wiki_service(self):
    advisor = RegexpMethodPointcutAdvisor(
    advice=[CachingInterceptor()],
    patterns=[".*get.*", ".*store.*"])
    return ProxyFactoryObject(
    target=WikiService(self.data_access()),
    interceptors=advisor)
    

Spring Python's RegexpMethodPointcutAdvisor is an out-of-the-box advisor that uses a set of regular expressions to define its pointcuts.

Note

As mentioned earlier, an advisor is Spring Python's implementation of an aspect. A pointcut is a definition of where to apply an aspect's Advice. In this case, the pointcut is defined as a regular expression. But not all pointcuts require regular expressions.

Each pattern is checked against the call stack's complete canonical method name (<package>.<class>.<method>) until a match is found (or the patterns are exhausted). If there is a match, the list of advisors is applied. Otherwise, it bypasses the list of advisors and instead directly calls the target. The following diagrams shows how these components are chained together; the first one depicts the sequence of requesting a specific article for the first time:

Adding caching to Spring Python objects

In the previous sequence of steps, the method name is checked to see if it matches the pattern for caching. Assuming that it does, it is forwarded to the caching interceptor. Then the cache is checked. If it hasn't been cached yet, it is fordward to WikiService. After reaching WikiService, the results are cached and then returned. The next diagram shows the sequence of steps when that article is requested again:

Adding caching to Spring Python objects

In this case, we again check the same regular expression pattern to see if the method qualifies for caching. Since it does, we next check the cache. Because it's there, we don't have to call WikiService, saving us from having to make a database call.

The final sequence shows what happens if we invoke a method that doesn't match our caching pattern.

Adding caching to Spring Python objects

In this situation, we again exercise the pattern match. But because it doesn't match, RegexpMethodPointcutAdvisor falls through to the target object, bypassing the caching advisor. We can go straight to WikiService and find up-to-date statistics.

Note

It is important to realize that while the caching interceptor reduces hits to the database, there is still an overhead cost of pattern matching on the method name. This type of overheard cost must be included in any end-to-end performance analysis.

This solves the problem of cleanly applying a caching service to the API of our wiki engine, without breaking the SRP. By using Spring Python's AOP module, we have been able to code a generic, re-usable caching module and plugged it in with no impact to our wiki API.

Applying many advisors to a service

Towards the end of coding our caching solution, we used RegexpMethodPointcutAdvisor to conditionally apply a list of advisors based on regular expression patterns. Spring Python's AOP solution supports applying more than one advisor to an object. This makes it easy to mix multiple services together for different objects, such as caching, transactions, security, and logging.

Spring Python makes it easy to add a service on top of an API. There is no limit on how many places a piece of advice can be reapplied. An example of a more complex and realistic configuration is shown below:

class WikiProductionAppConfig(PythonConfig):
def __init__(self):
super(WikiProductionAppConfig, self).__init__()
@Object
def data_access(self):
return MysqlDataAccess()
@Object
def security_advisor(self):
return RegexpMethodPointcutAdvisor(
advice=[SecurityInterceptor()],
patterns=[".*store.*"])
@Object
def perf_advisor(self):
return RegexpMethodPointcutAdvisor(
advice=[PerformanceInterceptor()],
patterns=[".*get.*"])
@Object
def caching_advisor(self):
return RegexpMethodPointcutAdvisor(
advice=[CachingInterceptor()],
patterns=[".*get.*", ".*store.*"])
@Object
def wiki_service(self):
return ProxyFactoryObject(
target=WikiService(self.data_access()),
interceptors=[self.security_advisor(),
self.perf_advisor(),
self.caching_advisor(),
self.perf_advisor()])

In this example, wiki_service has several advisors: security_advisor, perf_advisor, and caching_advisor. They are chained together in a stack, with perf_advisor being used twice. The following diagram shows the call stack of advisors and their interceptors, followed by a detailed explanation of what each advisor does.

Applying many advisors to a service
Applying many advisors to a service

security_advisor is an instance of RegexpMethodPointcutAdvisor that applies SecurityInterceptor against any method that begins with store.

class SecurityInterceptor(MethodInterceptor):
def invoke(self, invocation):
if user.has_access(invocation):
return invocation.proceed()
else:
raise SecurityException("Unauthorized Access")

SecurityInterceptor checks if the user's credentials are adequate to complete this operation. If access is granted, it calls invocation.proceed(), which flows on to the first instance of perf_advisor. If access is denied, it raises a security exception, breaking out of the entire call stack, leaving the caller to handle the exception. In this example, user is assumed to be a global variable containing the current user's profile.

perf_advisor is an instance of RegexpMethodPointcutAdvisor that applies PerformanceInterceptor against any methods that begin with get. While there is only one instance of perf_advisor, it is used twice in the chain of advisors. This means it will be used twice during the normal flow into WikiService.

import time
class PerformanceInterceptor(MethodInterceptor):
def invoke(self, invocation):
start = time.time()
results = invocation.proceed()
stop = time.time()
print "Method took %2f seconds" % (stop-start)
return results

PerformanceInterceptor measures the performance of a method by capturing system time, calling invocation.proceed(), and then capturing system time again when the invocation is complete. It then prints out the difference in times on the screen, and finally returns back to the calling advisor. The first time it is used is before caching_advisor, and the second time after caching_advisor. This allows measuring both cached and un-cached calls, measuring relief provided by the caching.

We've already seen how the CachingInterceptor works.

While the entire flow involved in making a call to WikiService is much more complicated than our earlier example, it was easy to quickly define extra services and apply them to our WikiService API. Each interceptor is neat and clean, and easy to understand. This helps lower maintenance costs. The interceptors are nicely decoupled from each other as well as WikiService, making it easier to mix and match aspects to suit our requirements. The exact sequence in which everything is wired together is conveniently kept in one place, our IoC container definition. This is shown by how we used perf_advisor twice, measuring performance with and without caching. This collectively demonstrates how easy it is to build new services and apply them to existing APIs.

Performance cost of AOP

AOP isn't free. There is a certain overhead cost involved with wrapping a target object with a Spring Python AOP proxy and performing checks on method calls. This clearly depends on how much advice you are using and what your pointcuts are. Using lots of regular expressions can get expensive, while simply applying an interceptor to every method with no conditional checks has a smaller cost.

There is also a different impact on whether or not the advised target object is inside a tight loop.

Tip

It is important to measure costs before optimizing. Premature optimization can result in wasted effort with little benefit. Using a Python profiler (or Java profiler when using Jython) is key to identifying performance bottlenecks.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.70.101