Wednesday, November 28, 2007

Child process keep alive: fork and CLD Signal

The advantage of a process over a thread is when the process dies you can get a signal telling you the process died and recover. For any long running process, it's always very important to be able to recover from unexpected disasters. In working on a server that's tasked with receiving a lot of large files and spending a fair bit of timing processing those files, I needed to make sure if for some reason the file input I received caused my server to go down - I could quickly recover. Because, I communicate to this server via a pipe (instead of a socket), it's not as simple to recover using something like monit.

The CLD signal is sent when a child process dies. This is great! All I need to do is trap that signal and start my process backup. My main concern with this is will I get stuck in an infinite loop, forking new processes because some condition has caused the child process to die every time, consuming all the resources on my system.

Here's my solution so far:
class UploadServer
def initialize(options = {})
@upload_read, @upload_write = IO.pipe
@start_up_threads = (options[:start_up_threads] || 2)
@max_read = (options[:max_read] || 1024)
@logger = (options[:logger] || Logger.new(STDOUT))
end

# starts up the upload server
def start
@pid = fork do
initialize_server
while( 1 )
select
end
end
Signal.trap(0) do
# tell the child process to die
Process.kill("TERM", @pid)
end
Signal.trap("CLD") do
# something extremely unexpected happened and the child process died
@logger.error( "It appears the upload background process has died... Attempting a restart..." )
# make sure we kill of any residue from the child process is cleaned up e.g. avoid defunct process
Process.wait(@pid)
# this is all a little risky since someone could have been in the middle of an upload
# they'll be cut off anyway since the process died...
# close down open pipe
@upload_write.close
# create a new pipe
@upload_read, @upload_write = IO.pipe
# start it back up
start
end
@logger.debug( "Upload Process started up on #{@pid}" )
# close the read end on the main process
@upload_read.close
@pid
end


The Process.wait(@pid) is very important otherwise we're left with a lot of <defunct> processes. It may also help to throttle the issue of infinite forking. At the very least it means we'll never get more then 1 child process per server. The only other thing I can imagine adding is some kind of timer to help throttle in the case that the child process dies very quickly...

Wednesday, November 21, 2007

dyndns change

After maybe four years of having dyndns setup to route traffic from http://severna.homeip.net to my home server to a static IP. They silently changed their policy on static IP vs dynamic IP addresses. Dynamic IP have always needed to be updated monthly or else they would disable the accounts. Static IP addresses however, in the past did not require this. As far as I can tell in early October they changed this policy. I decided it was time to setup a real domain name so I renamed the site to http://xullicious.com/ and for the time being have severna.homeip.net setup again, but now redirecting to http://xullicious.com/.

Monday, November 19, 2007

IO.pipe for interprocess communication

Pipes are a really simple way to send messages from one process to another.

MAX_READ = 4

rd, wd = IO.pipe

pid = fork do
wd.close

msg_buffer = ""
c = 0

while( 1 )
# select on the read end of the pipe
ready = IO.select( [rd], nil, nil, 1 )
if ready
ready[0].each do|io|
msg_buffer << begin
io.read_nonblock(MAX_READ)
rescue EOFError
puts "the pipe was unexpectidly closed??"
exit
rescue Object => e
STDERR.puts "failed with" + e.message + "\n" + e.backtrace("\n")
end

last_is_complete = msg_buffer.match(/\n\n$/)
messages = msg_buffer.split("\n\n")

# the last msg is not complete
if !last_is_complete
msg_buffer = messages.pop
else
msg_buffer = "" # reset the msg_buffer we're reading everything
end

messages.each do|msg|
puts msg
c += 1
end

puts c

end
end
end
end

rd.close

count = 10
while( count > 0 )
wd.write( "hello\n\nhello\n\nhello\n\n" )
wd.write( "hello\n\n" )
wd.write( "hello\n\nhello\n\nhello\n\n" )
wd.write( "hello\n\n" )
wd.write( "hello\n\n" )
sleep 0.1
count -= 1
end
wd.close

Wednesday, November 14, 2007

to_xml, almost deep enough


Workout
has_many :laps

Lap
has_many :points
belongs_to :workout

Point
belongs_to :lap


I'd like to serialize it all. The first approach


render :xml => workout.to_xml(:include => :laps)

Cool, we get workout data with laps, but no points.



Reading through the to_xml implementation it makes sense that you can only get to first level associations. For my purposes, I just wanted to avoid repeating my data model in my rxml files.



Here's what I came up with.


module Builder
class XmlMarkup
def add_record!(record,options = {})
options = {:builder => self,:skip_instruct => true}.merge!(options)
ActiveRecord::XmlSerializer.new(record, options).serialize
end

def add_record_attributes!(record)
record.attribute_names.each do|name|
attr = ActiveRecord::XmlSerializer::Attribute.new(name,record)
tag!( attr.name, attr.value.to_s, attr.decorations )
end
end
end
end



Now, in my rxml view I can write this.
xml.instruct!( :xml, :version=>"1.0", :encoding=>"UTF-8" )
xml.workout do
xml.add_record_attributes!(@workout)
xml.laps do
@workout.laps.each do|lap|
xml.add_record!(lap, :include => :points)
end
end
end

Reading list