The delegate system enables the use of custom code to customize the application's behavior. The code can be written as:
Regardless of language, the delegate class is instantiated per-request, early in the request cycle, and the instance is disposed of at the end of the request cycle. At various points in the request cycle, its methods are called by the application to obtain information needed to service the request.
Before any other methods are called, the application will set the request context, which is a Hash (Ruby) or RequestContext (Java) of request properties with perhaps some other useful information mixed in.
The code for the Ruby delegate exists in the form of a delegate class defined in a Ruby script file. See the documentation of the JRuby plugin for more information.
The Java delegate system relies on a delegate class that is very similar to the one used by the Ruby system, but instead is written in Java (or some other JVM language), compiled, and packed into a JAR file. Galia relies on the JDK's ServiceLoader to auto-discover the delegate class. The delegate class implements Delegate and, like all other plugins, Plugin.
Delegate methods may access a logger that writes to the application log. A Java delegate should acquire its own Logger as follows:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class MyClass {
private static final Logger LOGGER = LoggerFactory.getLogger(MyClass.class);
void doSomething() {
LOGGER.trace("Hello {}", "world");
LOGGER.debug("Hello world");
LOGGER.info("Hello world");
LOGGER.warn("Hello world");
LOGGER.error("Hello world");
}
}
A Ruby delegate may use a more convenient wrapper class:
require "java"
logger = Java::is.galia.plugin.jruby.Logger
logger.trace "Hello world"
logger.debug "Hello world"
logger.info "Hello world"
logger.warn "Hello world"
logger.error "Hello world"
Error stack traces may also be logged:
require "java"
logger = Java::is.galia.plugin.jruby.Logger
begin
raise "Something went wrong."
rescue => e
logger.error("#{e}", e)
end
Several delegate methods will be called over the course of a single request, and making them as efficient as possible will improve response times. A couple of ways to improve efficiency are:
Some methods may need to do similar work, which may be expensive. To avoid having to do the work more than once per request, a useful technique is to cache the first result. So, rather than doing this:
class CustomDelegate
def method1(options = {})
# perform an expensive query and return the result
end
def method2(options = {})
# perform the same expensive query and return the result
end
end
You could do this:
class CustomDelegate
def method1(options = {})
result = perform_expensive_query
end
def method2(options = {})
result = perform_expensive_query
end
# Performs an expensive query only once, caching the result.
def perform_expensive_query
unless @result
# perform the query
@result = ... # save the result in an instance variable
end
@result
end
end
This is an improvement, but it still results in at least one expensive query per request. It is also possible to cache the query result across requests, and there are a number of ways to do it. Here is an example utilizing a java.util.concurrent.ConcurrentHashMap, bearing in mind that the solution needs to be thread-safe:
require "java"
class CustomDelegate
# This is a static variable, accessible by all instances.
# We could also use a constant.
@@query_cache = Java::java.util.concurrent.ConcurrentHashMap.new
def method1(options = {})
result = fetch_result(context['identifier'])
end
def method2(options = {})
result = fetch_result(context['identifier'])
end
private
def fetch_result(identifier)
result = @@query_cache[identifier]
unless result
result = perform_expensive_query
@@query_cache[identifier] = result
end
result
end
def perform_expensive_query
# perform the query and return the result
end
end
Now there will be at most one expensive query per image identifier rather than one per request.
Of course, the contents of the map will be lost when the server is stopped. A more robust solution than any of these examples might involve an external cache server, which would also work more effectively with a cluster of application instances.
Many HTTP clients maintain an internal connection pool, but JDBC adapters do not. When accessing a database via JDBC, consider using a connection pool to improve performance. As of now, there is no official provision for this, but some options include:
Delegate methods can be tested by creating an instance of the CustomDelegate
class, setting its context to be similar to what the application would set it to, and calling a method:
# This file is named `test.rb`, in the same folder as `delegates.rb`
require "./delegates"
# Initialize the delegate
obj = CustomDelegate.new
obj.context = {
"identifier" => "identifier-to-test",
"client_ip" => "127.0.0.1",
"request_headers" => {
"X-SomeHeader" => "true",
"X-OtherHeader" => "false"
}
}
# Perform the test
raise "fail" unless obj.filesystemsource_path == "expected value"
This script can then be run on the command line with a command like: ruby test.rb
.
It should be possible to test delegate methods using any Java testing framework.