Monday, July 02, 2012

Ruby and Passing Methods/Functions

I've been writing some software to test connectivity to our server software. I started writing the code in Ruby primarily because it's easy to write. It also provides another language implementation of libraries to use against the server verifying that nothing written is specific to our particular language/setup.

One of the tests we needed was a testing a bunch of different connections to the server. To test the absolute max thru-put of the system JMeter was used. However, a test that was more of a simulation of users connecting was needed. Ruby has threads that are non-native and are only scheduled by the Ruby process. These aren't necessarily optimal, but may have been good enough.

So, what I decided to do was use Ruby's native system fork ability and spawn processes that would communicate with the server. The original code worked fine. However, I needed the ability to do process control in a more generic way that made it easy to spawn any simulation that I needed. To do this I ended up modifying the code to allow the passing of functions/methods to the process controller.

def process_control(num_process_to_spawn, param_for_func)
   ...
   func = nil
   num_process_to_spawn.times { |count|
   func = yield func
   pid = fork { 
      if func.call(param_for_func) = true
         #Exit after child func is finished. 
         Process.exit(true)
      else
         #Don't exit we're the parent
         Process.exit(false)
      end
   }
   ....
end

The above code calls forks and executes in the child the function call that is passed in via the yield. An example of how to use this is below.

<pre>
def function_count_num_times(num_times)
   num_times.times { |count| puts "Count: #{count}" } 
end

#Spawn 400 processes and pass 10 to the called function
process_control(400, 10) {  |func| 
   func = method(:function_count_num_times)
}

If you need to pass a method of an object it's pretty easy and you can setup the appropriate data for the method to work with ahead of time.

class TestControl
   attr_reader :internal_count
   def initialize()
      @internal_count = 0
   end

   def output(num_times)
      num_times.times { |count| 
         puts "count: #{count}"
      }
   end
end

#be careful of garbage collection
test_control_objects = Array.new

process_control(400, 10) { |func|
   ctrl_obj = TestControl.new
   test_control_objects.push(ctrl_obj)
   func = ctrl_obj.method(:output)
}