PageRenderTime 26ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/src/parallel.py

#
Python | 330 lines | 288 code | 2 blank | 40 comment | 5 complexity | 55758a8434a99ac0755dddcde9f1ad3e MD5 | raw file
Possible License(s): GPL-3.0
  1. #!/usr/bin/env python
  2. # must use python 2.4 or higher
  3. """
  4. If you use and like our software, please send us a postcard! ^^
  5. Copyright (C) 2009, 2010, Zhang Initiative Research Unit,
  6. Advance Science Institute, Riken
  7. 2-1 Hirosawa, Wako, Saitama 351-0198, Japan
  8. ---
  9. This program is free software: you can redistribute it and/or modify
  10. it under the terms of the GNU General Public License as published by
  11. the Free Software Foundation, either version 3 of the License, or
  12. (at your option) any later version.
  13. This program is distributed in the hope that it will be useful,
  14. but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. GNU General Public License for more details.
  17. You should have received a copy of the GNU General Public License
  18. along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. ---
  20. Read one command from each line of commands_file, execute several in
  21. parallel.
  22. 'xargs -P' has something almost similar, but not exactly what we need.
  23. Your machine's number of cores is the default parallelization factor.
  24. warning: keep this script compatible with python 2.4 so that we can run it
  25. on old systems too
  26. """
  27. import commands, os, socket, subprocess, sys, tempfile, time, thread
  28. import Pyro.core, Pyro.naming
  29. from optparse import OptionParser
  30. from threading import Thread
  31. from Queue import Queue, Empty
  32. from ProgressBar import ProgressBar
  33. from Pyro.errors import PyroError, NamingError, ConnectionClosedError
  34. from StringIO import StringIO
  35. from subprocess import Popen
  36. class Master(Pyro.core.ObjBase):
  37. def __init__(self, commands_q, results_q, begin_cmd = "", end_cmd = ""):
  38. Pyro.core.ObjBase.__init__(self)
  39. self.jobs_queue = commands_q
  40. self.results_queue = results_q
  41. self.lock = thread.allocate_lock()
  42. self.no_more_jobs = False
  43. self.begin_command = begin_cmd
  44. self.end_command = end_cmd
  45. def get_work(self, previous_result = None):
  46. if previous_result:
  47. self.results_queue.put(previous_result)
  48. res = None
  49. self.lock.acquire()
  50. if self.no_more_jobs:
  51. res = ""
  52. else:
  53. res = self.jobs_queue.get(True)
  54. if res == "END":
  55. self.no_more_jobs = True
  56. res = ""
  57. self.lock.release()
  58. return res
  59. def add_job(self, cmd):
  60. self.jobs_queue.put(cmd)
  61. def get_begin_end_commands(self):
  62. return (self.begin_command, self.end_command)
  63. def get_nb_procs():
  64. res = None
  65. try:
  66. # POSIX
  67. res = os.sysconf('SC_NPROCESSORS_ONLN')
  68. except:
  69. try:
  70. # {Free|Net|Open}BSD and MacOS X
  71. res = int(commands.getoutput("sysctl -n hw.ncpu"))
  72. except:
  73. res = 0
  74. return res
  75. def worker_wrapper(master, lock):
  76. begin_cmd = ""
  77. end_cmd = ""
  78. try:
  79. not_started = True
  80. while not_started:
  81. try:
  82. work = master.get_work()
  83. not_started = False
  84. except Pyro.errors.ProtocolError:
  85. print "warning: retrying master.get_work()"
  86. time.sleep(0.1)
  87. (begin_cmd, end_cmd) = master.get_begin_end_commands()
  88. if begin_cmd != "":
  89. print "worker start: %s" % commands.getoutput(begin_cmd)
  90. while work != "":
  91. # there is a bug in StringIO, calling getvalue on one where it
  92. # was never written throws an exception instead of returning an
  93. # empty string
  94. cmd_out = StringIO()
  95. cmd_out.write("i:%s" % work)
  96. cmd_stdout = tempfile.TemporaryFile()
  97. cmd_stderr = tempfile.TemporaryFile()
  98. p = Popen(work, shell=True, stdout=cmd_stdout, stderr=cmd_stderr,
  99. close_fds=True)
  100. p.wait() # wait for the command to complete
  101. # rewind its stdout and stderr files
  102. cmd_stdout.seek(0)
  103. cmd_stderr.seek(0)
  104. for l in cmd_stdout:
  105. cmd_out.write("o:%s" % l)
  106. cmd_stdout.close()
  107. for l in cmd_stderr:
  108. cmd_out.write("e:%s" % l)
  109. cmd_stderr.close()
  110. in_out_err = cmd_out.getvalue()
  111. cmd_out.close()
  112. # FBR: compression hook should be here
  113. # this could be pretty big stuff to send
  114. work = master.get_work(in_out_err)
  115. except ConnectionClosedError: # server closed because no more jobs to send
  116. pass
  117. #print "no more jobs for me, leaving"
  118. if end_cmd != "":
  119. print "worker stop: %s" % commands.getoutput(end_cmd)
  120. lock.release()
  121. default_pyro_port = 7766
  122. pyro_daemon_loop_cond = True
  123. # a pair parameter is required by start_new_thread,
  124. # hence the unused '_' parameter
  125. def master_wrapper(daemon, _):
  126. # start infinite loop
  127. print 'Master started'
  128. daemon.requestLoop(condition=lambda: pyro_daemon_loop_cond)
  129. optparse_usage = """Usage: %prog [options] {-i | -c} ...
  130. Execute commands in a parallel and/or distributed way."""
  131. my_parser = OptionParser(usage = optparse_usage)
  132. my_parser.add_option("-b", "--begin",
  133. dest = "begin_command", default = "",
  134. help = ("command run by a worker before any job "
  135. "(englobe command in parenthesis)"))
  136. my_parser.add_option("-c", "--client",
  137. dest = "server_name", default = None,
  138. help = ("read commands from a server instead of a file "
  139. "(incompatible with -i)"))
  140. my_parser.add_option("-d", "--demux",
  141. dest = "demuxer", default = None,
  142. help = "specify a demuxer, NOT IMPLEMENTED")
  143. my_parser.add_option("-e", "--end",
  144. dest = "end_command", default = "",
  145. help = "command run by a worker after last job "
  146. "(englobe command in parenthesis)")
  147. my_parser.add_option("-i", "--input",
  148. dest = "commands_file", default = None,
  149. help = ("/dev/stdin for example "
  150. "(incompatible with -c)"))
  151. my_parser.add_option("-m", "--mux",
  152. dest = "muxer", default = None,
  153. help = "specify a muxer, NOT IMPLEMENTED")
  154. my_parser.add_option("-o", "--output",
  155. dest = "output_file", default = None,
  156. help = "log to a file instead of stdout")
  157. my_parser.add_option("-p", "--port",
  158. dest = "server_port", default = default_pyro_port,
  159. help = ("use a specific port number instead of Pyro's "
  160. "default one (useful in case of firewall or "
  161. "to have several independant servers running "
  162. "on the same host computer)"))
  163. my_parser.add_option("--post-proc",
  164. dest = "post_proc", default = None,
  165. help = ("specify a Python post processing module "
  166. "(omit the '.py' extension)"))
  167. my_parser.add_option("-s", "--server",
  168. action = "store_true",
  169. dest = "is_server", default = False,
  170. help = "accept remote workers")
  171. my_parser.add_option("-v", "--verbose",
  172. action = "store_true",
  173. dest = "is_verbose", default = False,
  174. help = "enable progress bar")
  175. my_parser.add_option("-w", "--workers",
  176. dest = "nb_local_workers", default = None,
  177. help = ("number of local worker threads, "
  178. "must be >= 0, "
  179. "default is number of detected cores, very "
  180. "probably 0 if your OS is not Linux"))
  181. def usage():
  182. my_parser.print_help()
  183. sys.exit(0)
  184. if __name__ == '__main__':
  185. try:
  186. (options, optargs) = my_parser.parse_args()
  187. show_progress = options.is_verbose
  188. commands_file_option = options.commands_file
  189. read_from_file = commands_file_option
  190. output_file_option = options.output_file
  191. output_to_file = output_file_option
  192. remote_server_name = options.server_name
  193. connect_to_server = remote_server_name
  194. nb_workers = options.nb_local_workers
  195. nb_threads = get_nb_procs() # automatic detection
  196. has_nb_workers_option = nb_workers
  197. post_proc_option = options.post_proc
  198. post_proc_fun = None
  199. has_post_proc_option = post_proc_option
  200. is_server = options.is_server
  201. muxer = options.muxer
  202. demuxer = options.demuxer
  203. daemon = None
  204. if output_to_file:
  205. output_file = open(output_file_option, 'w')
  206. if read_from_file: # mandatory option
  207. commands_file = open(commands_file_option, 'r')
  208. elif not connect_to_server:
  209. print "-i or -c is mandatory"
  210. usage() # -h | --help falls here also
  211. if has_nb_workers_option:
  212. nb_threads = int(nb_workers)
  213. if nb_threads < 0:
  214. usage()
  215. elif nb_threads <= 0:
  216. print ("fatal: unable to find the number of CPU, "
  217. "use the -w option")
  218. usage()
  219. if has_post_proc_option:
  220. module = __import__(post_proc_option)
  221. post_proc_fun = module.post_proc
  222. # check options coherency
  223. if read_from_file and connect_to_server:
  224. print "error: -c and -i are exclusive"
  225. usage()
  226. commands_queue = Queue()
  227. results_queue = Queue()
  228. master = Master(commands_queue, results_queue,
  229. options.begin_command, options.end_command)
  230. nb_jobs = 0
  231. locks = []
  232. if is_server:
  233. Pyro.core.initServer()
  234. try:
  235. daemon = Pyro.core.Daemon(port = int(options.server_port),
  236. norange = True)
  237. except Pyro.errors.DaemonError:
  238. print "error: port already used, probably"
  239. sys.exit(1)
  240. # publish objects
  241. uri = daemon.connect(master, 'master')
  242. #print uri # debug
  243. thread.start_new_thread(master_wrapper, (daemon, None))
  244. if connect_to_server:
  245. # replace master by its proxy for the remote object
  246. uri = ("PYROLOC://" + remote_server_name + ":" +
  247. str(options.server_port) + "/master")
  248. #print uri # debug
  249. master = Pyro.core.getProxyForURI(uri)
  250. # start workers
  251. for i in range(nb_threads):
  252. l = thread.allocate_lock()
  253. l.acquire()
  254. locks.append(l)
  255. time.sleep(0.01) # dirty bug correction:
  256. # on multiproc machines, starting threads without
  257. # waiting makes Pyro output this sometimes:
  258. # "Pyro.errors.ProtocolError: unknown object ID"
  259. # It is like if the Pyro daemon is not ready yet
  260. # to handle many new client threads...
  261. thread.start_new_thread(worker_wrapper, (master, l))
  262. # feed workers
  263. if read_from_file:
  264. # read jobs from local file
  265. for cmd in commands_file:
  266. master.add_job(cmd)
  267. nb_jobs += 1
  268. master.add_job("END")
  269. if read_from_file:
  270. progress_bar = ProgressBar(0, nb_jobs)
  271. # output everything
  272. jobs_done = 0
  273. if show_progress:
  274. progress_bar.draw()
  275. while jobs_done < nb_jobs:
  276. cmd_and_output = results_queue.get()
  277. jobs_done += 1
  278. # FBR: more code factorization possible here
  279. # if there is a default post_proc function which
  280. # is the identity function
  281. if output_to_file:
  282. if has_post_proc_option:
  283. output_file.write(post_proc_fun(cmd_and_output))
  284. else:
  285. output_file.write(cmd_and_output)
  286. if show_progress:
  287. progress_bar.update(jobs_done)
  288. progress_bar.draw()
  289. elif not output_to_file:
  290. if has_post_proc_option:
  291. sys.stdout.write(post_proc_fun(cmd_and_output))
  292. else:
  293. sys.stdout.write(cmd_and_output)
  294. # cleanup
  295. pyro_daemon_loop_cond = False
  296. commands_file.close()
  297. if output_to_file:
  298. output_file.close()
  299. # wait for everybody
  300. for l in locks:
  301. l.acquire()
  302. # stop pyro server-side stuff
  303. if is_server:
  304. daemon.disconnect(master)
  305. daemon.shutdown()
  306. except SystemExit:
  307. pass
  308. except: # unexpected one
  309. print "exception: ", sys.exc_info()[0]