Skip to content

Caching

Alex Vigdor edited this page Mar 26, 2018 · 1 revision

Groovity caching

Groovity comes with built-in support for pull-through caching, allowing you to concisely state caching rules and perform batch loading, and makes it as hard as possible for you to do the wrong things by accident. For example you can and should specify a maximum cache size, but Groovity leverages SoftReferences to make sure that a large cache never leads to an OutOfMemory condition. Groovity also creates strict binding isolation so that your batch loader cannot rely on anything other than cache keys to look up values, nor can your batch loader add anything directly to the normal process binding - these types of slipups can lead to nondeterministic behavior and difficult to reproduce bugs.

Each groovity script will get its own isolated, named set of Caches to go with its isolated ClassLoader; this makes it possible to safely cache your own inner classes written in Groovity, and to avoid potential naming collisions or other headaches across scripts.

Consider this function from the sample application:

public Map readNotes(List<Integer> ids){
	cache(name:"noteCache",keys:ids,refresh:30,ttl:120,max:10000,{
		load('/groovity/sql').sql('sampleDB')
			.eachRow("SELECT * FROM notes WHERE id in (${map.keySet().join(',')})".toString(),
				{ row ->
					map.put(row.id as Long,row.toRowResult() as Note);
				})
	})
}

Let's break it down:

  • cache() - a tag function that acquires a named cache and retrieves one or more objects in a pull-through fashion
  • name: - the name of the cache to use, can be left blank to use a default cache
  • key or keys: - one or a collection of key objects used to retrieve elements from the cache. The cache loader has no access to the script binding, to enforce that all information needed to locate a cache entry must be contained within the key, and to allow background threads to refresh entries in the cache
  • refresh: - a cache HIT more than 30 seconds after the cache entry was created will trigger a background batched refresh for that element; refresh creates a new cache entry so that frequently accessed objects stay up-to-date without expiring and forcing a synchronous load
  • ttl: - the longest a cache entry may live in the cache before being expired
  • max: - the maximum size the cache may reach before triggering LRU cleanup
  • {} - the final argument to cache() is a cache-loading closure that executes in its own isolated scope; it has no access to the script fields or binding, instead its binding consists of only a single entry called map. This map initially contains entries for every key that needs to be loaded, with null values; the closure must iterate through this map and populate the entries with values. The closure may make optimized bulk retrieval operations, as in this example with the SELECT WHERE IN, to populate the map, or it may itself perform multiple operations in parallel, e.g. by using the http() tag with async:true.

One thing you will notice is that there is no code to load an individual object; single-key loading is treated as a special case of multi-key loading inside the framework, so all you have to do is implement the optimal solution.

Here's a sample groovity servlet script that formats set of RSS feeds, using the cache tag combined with async http and streaming templates for maximum performance.

static web=[
	path:'/aggregator',
	output:"text/html"
]

urls = [
	'http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml',
	'http://feeds.abcnews.com/abcnews/politicsheadlines'
]

feeds = cache(keys:urls,refresh:30,ttl:60,max:100){
	//set up parallel async loading
	map.each{ entry ->
		entry.value = async{
			http(url:entry.key){
				handler{
					def val = parse(value:httpResponse)
					<~
					<div>
						<strong>${val.channel.title}</strong>
						<ul>
							<g:each var="item" in = "${val.channel.item}">
								<li><a href="${item.link}">${item.title}</a></li>
							</g:each>
						</ul>
					</div>
					~>.toString()
				}
			}
		}
	}
}

<~
<html>
<body>
Feeds
<g:each var="feed" in = "${feeds.values()}">
	${feed}
</g:each>
</body>
</html>
~>