Hints for running traject as a batch job

Maybe as a cronjob. Maybe via a batch shell script that executes traject, and maybe even pipelines it together with other commands.

These are things you might want to do with traject. Some potential problem points with suggested solutions, and additional hints.

Ruby version setting

For best performance, traject should run under jruby. You will ordinarily have jruby installed under a ruby version switcher -- we recommend chruby over other choices, but other popular choices include rvm and rbenv.

Especially when running under a cron job, it can be difficult to set things up so traject runs under jruby -- and then when you add bundler into it, things can get positively byzantine. It's not you, this gets confusing.

It can sometimes be useful to create a wrapper script for traject that takes care of making sure it's running under the right ruby version.

for chruby

Simply run with:

chruby-exec jruby -- traject {other arguments}

Whether specifying that directly in a crontab, or in a shell script that needs to call traject, etc. In a crontab environment, it'll actually need you to set PATH and SHELL variables, as specified in the chruby docs

So simple you might not need a wrapper script, but it might still be convenient to create one. Say you put a jruby-traject at /usr/local/bin/jruby-traject, that looks like this:

#!/usr/bin/env bash

chruby-exec jruby -- traject "$@"

Now you can can just execute jruby-traject {arguments}, and execute traject in a jruby environment. (In a crontab, you'll still need to fix your PATH and SHELL env variables for chruby-exec to work, either in the crontab or in this wrapper script)

chruby monster wrapper script

I am still not sure if this is a good idea, but here's an example of a wrapper script for chruby that will take care of the ENV even when running in a crontab, use chruby-exec only if jruby isn't already the default ruby, and add in bundle exec too.

#!/usr/bin/env bash

# A wrapper for traject that uses chruby to make sure jruby
# is being used before calling traject, and then calls
# traject with bundle exec from within our traject project
# dir. 

# Make sure /usr/local/bin is in PATH for chruby-exec,
# which it's not ordinarily in a cronjob. 
if [[ ":$PATH:" != *":/usr/local/bin:"* ]]
then
  export PATH=$PATH:/usr/local/bin
fi
# chruby needs SHELL set, which it won't be from a crontab
export SHELL=/bin/bash

# Find the dir based on location of this wrapper script,
# then use that dir to cd to for the bundle exec to find
# the right Gemfile. 
traject_dir=$(cd `dirname "${BASH_SOURCE[0]}"` && pwd)

# do we need to use chruby to switch to jruby?
if [[ "$(ruby -v)" == *jruby* ]]
then
  ruby_picker="" # nothing needed "
else
  ruby_picker="chruby-exec jruby --"
fi

cmd="BUNDLE_GEMFILE=$traject_dir/Gemfile $ruby_picker bundle exec traject $@"

echo $cmd
eval $cmd

This monster script can perhaps be adapted for rbenv or rvm.

for rbenv

If running in an interactive shell that has had rbenv set up for it, you can use rbenv's standard mechanism to say to execute something in jruby:

RBENV_VERSION=jruby-1.7.2 traject {args}

You do need to specify the exact version of jruby, I don't think there's any way to say 'latest install jruby'. You could do the same thing for any batch scripts you're writing -- just have them set that RBENV_VERSION environment variable before executing traject.

If you're running inside a cronjob, things get a bit trickier, because rbenv isn't normally set up in the limited environment of cron tasks. One way to deal with this is to have your cronjob explicitly execute in a bash login shell, that will then have rbenv set up -- so long as it's running under an account with rbenv set up properly!

# in a cronfile
# 10 * * * * /bin/bash -l -c 'RBENV_VERSION=jruby-1.7.2 traject {args}'

(Better way? Doc pull requests welcome.)

for rvm

See rvm's own docs on use with cron, it gets a bit confusing. But here's one way, using a wrapper script. It does require you to identify and hard-code in where your rvm is installed, and exactly which version of jruby you want to execute with (will have to be updated if you upgrade jruby). (Is there a better way? Doc pull requests welcome! rvm confuses me!)

Make a file at /usr/local/bin/jruby-traject that looks like this:

#!/usr/bin/env bash

# load rvm ruby
source /home/MY_ACCT/.rvm/environments/jruby-1.7.3

traject "$@"

You have to use your actual account rvm is installed in for MY_ACCT. Or, if you have a global install of rvm instead of a user-account one, it might be at /usr/local/rvm/environments... instead.

Now any account, in a crontab, in an interactive shell, wherever, can just execute jruby-traject {arguments}, and execute traject in a jruby environment.

Bundler too?

If you're running with bundler too, you could make a wrapper file specific to a particular traject project and it's Gemfile, by combining the bundle exec into your wrapper file. For instance, for chruby, this works:

#!/usr/bin/env bash

chruby-exec jruby -- BUNDLE_GEMFILE=/path/to/Gemfile bundle exec traject "$@"

Now you can call your wrapper script from anywhere and with any active ruby, and execute it in jruby and with the dependencies specified in the Gemfile for your project.

Exit codes

Traject tries to always return a well-behaved unix exit code -- 0 for success, non-0 for error.

You should be able to rely on this in your batch bash scripts, if you want to abort further processing if traject failed for some reason, you can check traject's exit code.

If an uncaught exception happens, traject will return non-0.

There are some kinds of errors which prevent traject from indexing one or more records, but traject may still continue processing the other records. If any records have been skipped in this way, traject will also return a non-0 failure exit code. (Is this good? Does it need to be configurable?)

In these cases, information about errors that led to skipped records should be output as ERROR level in the logs.

Logs and Error Reporting

By default, traject outputs all logging to stderr. This is often just what you want for a batch or automated process, where there might be some wrapper script which captures stderr and puts it where you want it.

However, it's easy enough to tell traject to log somewhere else. Either on the command-line:

traject -s log.file=/some/other/file/log {other args}

Or in a traject configuration file, setting the log.file configuration setting.

separate error log

You can also separately have a duplicate log file created with ONLY log messages of level ERROR and higher (meaning ERROR and FATAL), with the log.error_file setting. Then, if there's any lines in this error log file at all, you know something bad happened, maybe your batch process needs to notify someone, or abort further steps in the batch process.

traject -s log.file=/var/log/traject.log -s log.error_file=/var/log/traject_error.log {more args}

The error lines will be in the main log file, and also duplicated in the error log file.

Completely customizable logging with yell

Traject uses the yell gem for logging. You can configure the logger directly to implement whatever crazy logging rules you might want, so long as yell supports them. But yell is pretty flexible.

Recall that traject config files are just ruby, executed in the context of a Traject::Indexer. You can set the Indexer's logger to a yell logger object you configure yourself however you like:

  # inside a traject configuration file

  self.logger = Yell.new do |l|
    l.level = 'gte.info' # will only pass :info and above to the adapters

    l.adapter :datefile, 'production.log', level: 'lte.warn' # anything lower or equal to :warn
    l.adapter :datefile, 'error.log', level: 'gte.error' # anything greater or equal to :error
  end

note it's important to use to use self.logger =, or due to ruby idiosyncracies you'll just be setting a local variable, not the Indexer's logger attribute.

See yell docs for more, you can do whatever you can make yell, just write ruby.

Bundler

For automated batch execution, we recommend you consider using bundler to manage any gem dependencies. See the Extending With Your Own Code traject docs for information on how traject integrates with bundler.