Overview
Distribute a list of tasks (as shell commands) across different workers (local or remote).
Aggregate the commands' output (stdout, stderr, exit_code)
The main motivation behind this project is to make rspec run in parallel and distributed.
Generic workflow
- start RabbitMQ
- start worker(s) - separate process(es)
- start manager (programatically)
- manager pushes commands to the queue
- workers pick up command and execute them
- once command execution is done the output is returned to manager via RabbitMQ
- returned results are processed on manager's side
- stop workers when all commands are completed
- stop RabbitMQ if it's not needed anymore
Use cases
Pre-requisites
- 
radagastgem has been installed
- 
RabbitMQis available at default location:amqp://guest:guest@localhost:5672
To easily spin up RabbitMQ locally please see scripts in bin/rabbit-*.sh.
Manager and a single local worker
# start the worker
radagast# Example manager code (see examples/basic.rb)
require 'radagast'
manager = Radagast::Manager.new
manager.start
manager.task 'echo test1' do |result|
  puts result.exit_code  # 0
  puts result.stdout     # "test1"
  puts result.stderr     # ""
end
manager.task 'cat /etc/shadow' do |result|
  puts result.exit_code  # 1
  puts result.stdout     # ""
  puts result.stderr     # "cat: /etc/shadow: Permission denied"
end
manager.finish do |results|
  puts results.count     # 2
endManager and multiple workers (hosts)
# run this command on multiple hosts
radagast --rabbit amqp://user:password@rabbit.intranet:5672Manager code requires slight adjustments
require 'radagast'
config = Radagast::Config.new
config.rabbit = 'amqp://user:password@rabbit.intranet:5672'
manager = Radagast::Manager.new config
manager.start
# the rest remains the sameRunning rspec
# workers are running and manager is started
# example slicing of rspec test suite by tags.
# Slices will be excecuted paralelly on all connected workers
manager.task 'rspec --format RspecJunitFormatter --tag integration', slice: 1
manager.task 'rspec --format RspecJunitFormatter --tag selenium_subset2', slice: 2
manager.task 'rspec --format RspecJunitFormatter --tag selenium_subset2', slice: 3
manager.task 'rspec --format RspecJunitFormatter --tag performance', slice: 4
manager.task 'rspec --format RspecJunitFormatter --tag unit', slice: 5
manager.finish do |results|
  results.each do |result|
    File.write("results/rspec_slice#{result.meta['slice']}.xml", result.stdout)
  end
end
# now CI engine can merge all XML files stored in results/rspec_slice*.xmlScaling Workers with Docker (Swarm)
WARNING: THIS PART HAS NOT BEEN FULLY TESTED YET
Docker image to be used as a Worker has to include:
- radagast command (e.g. installed via bundleramong othergems)
- the software required to run the commands requested by Manager
Once these requirements are met Worker can be started with a command like this:
docker run -d \
  --entrypoint /path/to/radagast \
  --name radagast-worker-myapp-1 \
  myapp:v17.2.1 \
  --rabbit amqp://user:password@rabbit.intranet:5672Where myapp:v17.2.1 is the docker image name, and the lines below it (command passed to container)
contains the arguments for radagast command passed as the --entrypoint.
Multiple containers can be spawned this way, please just make sure that --name is unique.
Set up docker swarm to distribute workers across multiple nodes. No changes on radagast end are required.
docker run will automatically spawn workers on other network nodes as long as the environment variable DOCKER_HOST
points to a properly configured docker swarm.
API
At this stage Radagast consists of only a few classes with a pretty straight-forward API.
Config
A Struct containig the following fields (with defaults):
key = 'default'            # prefix used for RabbitMQ queue name. Optional, use with shared RabbitMQ instance
rabbit = 'amqp://guest:guest@127.0.0.1:5672'
log_level = Logger::INFO   # config for Logger
log_file = STDOUT          # config for LoggerWhen starting Worker via CLI with radagast command the following switches are supported and mapped into the Config instance.
--key KEY                  # prefix used for RabbitMQ queue name. Optional, use with shared RabbitMQ instance
--rabbit RABBIT            # rabbitmq URL
--log_level LOG_LEVEL      # has to properly evaluate by const_get. Example: Logger::DEBUG
--log_file LOG_FILE        # path to log file. Default is STDOUT (as seen above)See RabbitMQ docs for format of --rabbit URL.
Manager
# constructor
manager = Radagast::Manager.new config   # config is a Radagast::Config instance
manager = Radagast::Manager.new          # use default Config values
# start listening to results queue
manager.start
# publish task, don't care about the result yet
manager.task 'rspec --tag unit'
# publish task, tag it for better filtering of complete results list
manager.task 'rspec --tag unit', my_tag: 'my_value', other_tag: 2
# publish task, pass a block to process the result
manager.task 'rspec --tag unit' do |result|
  # result is an instance of Radagast::Result
  puts "Got some errors: #{result.stderr}" unless result.exit_code == 0
end
# wait for all results to arrive and process them
manager.finish do |results|
  # results is an array of Radagast::Result
  puts "Number of non-zero exit codes: #{results.count { |r| r.exit_code != 0 }}"
endResult
A Struct containig the following fields:
exit_code  # exit code from command execution
stdout     # command's output to stdout
stderr     # command's output to stderr
meta       # a hash of tags passed to Manager#task metod plus the 'cmd'
task_id    # used internally as an ID to handle callbacksWorker
In case one would need to start the Worker programatically.
# constructor
worker = Radagast::Worker.new config   # config is a Radagast::Config instance
worker = Radagast::Worker.new          # use default Config values
# start listening to tasks queue and react on incoming tasks
worker.start
# stop listening and do the clean up
worker.finishSee Config section above to learn how to configure worker executed from CLI.
Final thoughts
- The project is an early pre-beta. It has not been tested in production.
- It has been barely tested in development.
- Feel free to play around, submit bugs and features though
Why Radagast?
To honor Radagast the Brown: the first wizard who employed rabbits to move faster.