Galia Guide 1.0

Delegate System

The delegate system enables the use of custom code to customize the application's behavior. The code can be written as:

  1. A Ruby delegate script. This method is designed for ease of use. Ruby is a high-level language that is easy to learn and work with, and only small amounts of code are needed to support most use cases. Ruby support is not built into the core, and requires the JRuby plugin.
  2. A Java delegate. This is an alternative for developers who are more comfortable with Java or require faster startup times.

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.

Generally, neither the context, method arguments, nor return values are sanitized or validated. Take care to write defensive, injection-safe code.

Ruby delegate

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.

Java delegate

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.

See the example Java delegate for a minimal working example.

Logging

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

Improving efficiency

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:

Sharing information

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.

Connection pooling

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:

  1. Bundling a connection pooling library into the plugin (if using a Java delegate) or adding the library's JAR to the classpath, and invoking it over the JRuby-Java bridge.
  2. Write your own connection pooling code

Testing delegate methods

Ruby

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.

The ruby command will normally launch a standard ("YARV") Ruby interpreter, and not the JRuby interpreter. While they work pretty similar, gems with platform-native extensions won't work in JRuby. Consider installing a standalone JRuby interpreter and testing with that instead. (A tool like chruby or rbenv can make it easy to switch between different Ruby interpreters.)

Java

It should be possible to test delegate methods using any Java testing framework.