#!/usr/bin/env python usage = """ Demonstrate a bug on Mac OS X whereby Python can hang while simultaneously trying os.write with a large buffer and os.close, from separate threads on the write and read fds respectively of a pipe. usage: os_pipe_write_close_bug size logging [count] size power-of-two exponent for size of string used to demonstrate the problem; no hang seen with 16 or smaller, hangs with 17 or larger logging 0 for no call logging; 1 for call logging count optional, non-negative number of cycles to try; default is no count limit The problem has only been seen with size of 17 or larger (buffer 128K or larger). So, a workaround is to limit the buffer to 64K. Problem has been seen on Mac OS X 10.5 using Apple's Python 2.5.1: 2.5.1 (r251:54863, Feb 9 2009, 18:49:36) [GCC 4.0.1 (Apple Inc. build 5465)] and using MacPorts 1.8.1. Python 2.6.2 (python26 @2.6.2_4+darwin): 2.6.2 (r262:71600, Sep 14 2009, 20:46:20) [GCC 4.0.1 (Apple Inc. build 5493)] The problem has not been seen on 32-bit Ubuntu or 64-bit Red Hat. By default, this script cycles forever, or until the hang, printing out the cycle count and, optionally, line-by-line records of Python code that's been executed. All output is to stderr. When the process hangs, according to ps it's in an uninteruptible wait 'U+', so it's slightly tough to kill. If run from bash, Ctrl-Z followed by 'kill %1 %1' followed by two newlines does the trick. You can expect the hang to happen sooner (lower cycle count) with no logging, often fewer than 100 cycles. With logging turned on it often takes several hundred cycles before the hang occurs. From numerous runs with logging is turned on, the output from the cycle that hangs is always the same, e.g.: cycle: 919 os_pipe_write_close_bug: begin os_pipe_write_close_bug: try writer: writer: try os_pipe_write_close_bug: writer_thread.start() os_pipe_write_close_bug: finally But, this is also the prefix of the output from many successful cycles. """ import os, sys import threading, errno import itertools, time def main(args): limit = None try: assert len(args) == 2 or len(args) == 3 exponent = int(args[0]) logging = bool(int(args[1])) if len(args) == 3: limit = int(args[2]) assert limit >= 0 except: print >> sys.stderr, usage return -1 print >> sys.stderr, sys.version bytes = 'X' * (1 << exponent) for i in itertools.count() if limit is None else xrange(limit): print >> sys.stderr, 'cycle: ' + str(i) os_pipe_write_close_bug(bytes, logging) time.sleep(0) def os_pipe_write_close_bug(bytes, logging): if logging: print >> sys.stderr, ' os_pipe_write_close_bug: begin' fd_read, fd_write = os.pipe() def writer(): if logging: print >> sys.stderr, ' writer:\n', try: if logging: print >> sys.stderr, ' writer: try\n', os.write(fd_write, bytes) if logging: print >> sys.stderr, ' writer: os.write()\n', except OSError, err: if logging: print >> sys.stderr, ' writer: except OSError err\n', if err.errno != errno.EPIPE: if logging: print >> sys.stderr, ' writer: err.errno != errno.EPIPE\n', raise finally: if logging: print >> sys.stderr, ' writer: finally\n', os.close(fd_write) if logging: print >> sys.stderr, ' writer: os.close()\n', writer_thread = threading.Thread(target=writer) writer_thread.setDaemon(True) try: if logging: print >> sys.stderr, ' os_pipe_write_close_bug: try\n', writer_thread.start() if logging: print >> sys.stderr, ' os_pipe_write_close_bug: writer_thread.start()\n', finally: if logging: print >> sys.stderr, ' os_pipe_write_close_bug: finally\n', os.close(fd_read) if logging: print >> sys.stderr, ' os_pipe_write_close_bug: os.close()\n', writer_thread.join() if logging: print >> sys.stderr, ' os_pipe_write_close_bug: writer_thread.join()' if logging: print >> sys.stderr, ' os_pipe_write_close_bug: end' if __name__ == '__main__': sys.exit(main(sys.argv[1:]))