Forking

slyphon edited this page May 22, 2012 · 5 revisions
Clone this wiki locally

Forking, as it turns out, is very difficult to get right when you're writing a client. There is in 1.5.0 a best practice for forking, which is you should call ZK.install_fork_hook after requiring zk.

The right way to do it

require 'zk'
ZK.install_fork_hook

zk = ZK.new

fork do
  zk.wait_until_connected # this should happen almost immediately
  zk.create("/client/#{$$}", "OMG! you forked me!", :ephemeral => true)
end

What this does is install a wrapper around Kernel.fork that:

  • pauses all connections by stopping all threads except the calling thread
  • calls fork()
  • unpauses all connections in the parent
  • reopens connections in the child

The reason this complexity is necessary is that zookeeper has an asynchronous component, which involves running threads which are calling into a native client library. This can be dangerous, as the ruby interpreter does not deal well with cleaning up threading primitives, so after weeks of testing and crashing (and swearing), this has proven to be the most stable solution.

Bonus Points

A slightly more involved example shows that your registered callbacks (ec. the on_connected callback) in your parent will persist across the fork and reopen, so your child will have the same callback state as the parent.

#!/usr/bin/env ruby

require 'zk'
ZK.install_fork_hook

PARENT_PID = Process.pid

def debug(msg)
  pc_str = (PARENT_PID == Process.pid) ? 'parent' : 'child'
  $stderr.puts "[#{pc_str}] #{msg}"
end

zk = ZK.new do |z|
  z.on_connected do |event|
    debug "on_connected called"
  end
end

debug "session_id: 0x%x" % [zk.session_id]

pid = fork do
  zk.wait_until_connected 
  p zk.children('/')
  debug "session_id: 0x%x" % [zk.session_id]
end

_, status = Process.wait2(pid)

debug "child exited with status #{status.inspect}"

debug "session_id: 0x%x" % [zk.session_id]

produces the output:

[parent] session_id: 0x1372adb6ece4344
[parent] on_connected called
[child] on_connected called
["_zklocking", "test", "one-level", "pid", "testwatchers", "zookeeper", "_zkqueues"]
[child] session_id: 0x1372adb6ece4345 
[parent] child exited with status #<Process::Status: pid 12538 exit 0>
[parent] session_id: 0x1372adb6ece4344

You can see that the child gets a new session id from the ZooKeeper server.

The on_connected handler is a good place to set up zk.stat(path, :watch => true) type calls to begin receiving event notifications, as it will be called in the case of a session failure, and also in the child when you fork. You do not need to re-register your subscriptions for events (i.e. zk.register(path)) as that state will survive the fork and be set up in the child process.