├── THANKS
├── examples
├── static
│ ├── hello.html
│ ├── images
│ │ ├── logo.png
│ │ ├── gunicorn.png
│ │ └── large_gunicorn.png
│ └── index.html
├── .hello.py.swo
├── hello_pool.py
├── hello.py
├── multiworker.py
├── tcp_hello.py
├── multiworker2.py
├── _sendfile.py
├── amqp.py
└── serve_file.py
├── pistil
├── tcp
│ ├── __init__.py
│ ├── arbiter.py
│ ├── sync_worker.py
│ ├── gevent_worker.py
│ └── sock.py
├── __init__.py
├── errors.py
├── workertmp.py
├── pidfile.py
├── worker.py
├── pool.py
├── util.py
└── arbiter.py
├── MANIFEST.in
├── .gitignore
├── NOTICE
├── LICENSE
├── setup.py
└── README.rst
/THANKS:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/static/hello.html:
--------------------------------------------------------------------------------
1 | hello world
2 |
--------------------------------------------------------------------------------
/examples/.hello.py.swo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meebo/pistil/HEAD/examples/.hello.py.swo
--------------------------------------------------------------------------------
/examples/static/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meebo/pistil/HEAD/examples/static/images/logo.png
--------------------------------------------------------------------------------
/examples/static/images/gunicorn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meebo/pistil/HEAD/examples/static/images/gunicorn.png
--------------------------------------------------------------------------------
/pistil/tcp/__init__.py:
--------------------------------------------------------------------------------
1 | from pistil.tcp.arbiter import TcpArbiter
2 | from pistil.tcp.sync_worker import TcpSyncWorker
3 |
--------------------------------------------------------------------------------
/examples/static/images/large_gunicorn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meebo/pistil/HEAD/examples/static/images/large_gunicorn.png
--------------------------------------------------------------------------------
/examples/static/index.html:
--------------------------------------------------------------------------------
1 | Welcome !
2 |
3 | Go to Hello world.
4 |
5 | Or list images/
6 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include .gitignore
2 | include LICENSE
3 | include NOTICE
4 | include README.rst
5 | include THANKS
6 |
7 | recursive-include examples *
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.swp
3 | *.pyc
4 | *#*
5 | build
6 | dist
7 | setuptools-*
8 | .svn/*
9 | .DS_Store
10 | *.so
11 | distribute-0.6.8-py2.6.egg
12 | distribute-0.6.8.tar.gz
13 | pistil.egg-info
14 | nohup.out
15 | .coverage
16 | doc/.sass-cache
17 |
--------------------------------------------------------------------------------
/pistil/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of pistil released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 |
7 | version_info = (0, 2,0)
8 | __version__ = ".".join(map(str, version_info))
9 |
10 | SERVER_SOFTWARE = "pistil/%s" % __version__
11 |
--------------------------------------------------------------------------------
/pistil/errors.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of pistil released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 |
7 | class HaltServer(Exception):
8 | def __init__(self, reason, exit_status=1):
9 | self.reason = reason
10 | self.exit_status = exit_status
11 |
12 | def __str__(self):
13 | return "" % (self.reason, self.exit_status)
14 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | Pistil
2 |
3 | 2011 (c) Benoît Chesneau
4 | 2011 (c) Meebo
5 |
6 | Pistil is released under the MIT license. See the LICENSE
7 | file for the complete license.
8 |
9 |
10 | Following files are under MIT License and copyrights:
11 | 2009-2011 (c) Benoît Chesneau
12 | 2009-2011 (c) Paul J. Davis
13 |
14 |
15 | pistil/arbiter.py,
16 | pistil/util.py
17 | pistil/sock.py
18 | pistil/pidfile.py
19 | pistil/workertmp.py
20 | pistil/worker.py
21 |
--------------------------------------------------------------------------------
/examples/hello_pool.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of pistil released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 | from pistil.pool import PoolArbiter
7 | from pistil.worker import Worker
8 |
9 | class MyWorker(Worker):
10 |
11 | def handle(self):
12 | print "hello from worker n°%s" % self.pid
13 |
14 | if __name__ == "__main__":
15 | conf = {"num_workers": 3 }
16 | spec = (MyWorker, 30, "worker", {}, "test",)
17 | a = PoolArbiter(conf, spec)
18 | a.run()
19 |
--------------------------------------------------------------------------------
/examples/hello.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of pistil released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 | import time
7 |
8 | from pistil.arbiter import Arbiter
9 | from pistil.worker import Worker
10 |
11 |
12 | class MyWorker(Worker):
13 |
14 | def handle(self):
15 | print "hello from worker n°%s" % self.pid
16 |
17 |
18 | if __name__ == "__main__":
19 | conf = {}
20 | specs = [(MyWorker, 30, "worker", {}, "test")]
21 | a = Arbiter(conf, specs)
22 | a.run()
23 |
--------------------------------------------------------------------------------
/examples/multiworker.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of pistil released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 | from pistil.arbiter import Arbiter
7 | from pistil.worker import Worker
8 |
9 |
10 | class MyWorker(Worker):
11 |
12 | def handle(self):
13 | print "hello worker 1 from %s" % self.name
14 |
15 | class MyWorker2(Worker):
16 |
17 | def handle(self):
18 | print "hello worker 2 from %s" % self.name
19 |
20 |
21 | if __name__ == '__main__':
22 | conf = {}
23 |
24 | specs = [
25 | (MyWorker, 30, "worker", {}, "w1"),
26 | (MyWorker2, 30, "worker", {}, "w2"),
27 | (MyWorker2, 30, "kill", {}, "w3")
28 | ]
29 | # launchh the arbiter
30 | arbiter = Arbiter(conf, specs)
31 | arbiter.run()
32 |
--------------------------------------------------------------------------------
/examples/tcp_hello.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of pistil released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 | from pistil.tcp.sync_worker import TcpSyncWorker
7 | from pistil.tcp.arbiter import TcpArbiter
8 |
9 | from http_parser.http import HttpStream
10 | from http_parser.reader import SocketReader
11 |
12 | class MyTcpWorker(TcpSyncWorker):
13 |
14 | def handle(self, sock, addr):
15 | p = HttpStream(SocketReader(sock))
16 |
17 | path = p.path()
18 | data = "hello world"
19 | sock.send("".join(["HTTP/1.1 200 OK\r\n",
20 | "Content-Type: text/html\r\n",
21 | "Content-Length:" + str(len(data)) + "\r\n",
22 | "Connection: close\r\n\r\n",
23 | data]))
24 |
25 | if __name__ == '__main__':
26 | conf = {"num_workers": 3}
27 | spec = (MyTcpWorker, 30, "worker", {}, "worker",)
28 |
29 | arbiter = TcpArbiter(conf, spec)
30 |
31 | arbiter.run()
32 |
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | 2011 (c) Benoît Chesneau
2 | 2011 (c) Meebo
3 |
4 | Permission is hereby granted, free of charge, to any person
5 | obtaining a copy of this software and associated documentation
6 | files (the "Software"), to deal in the Software without
7 | restriction, including without limitation the rights to use,
8 | copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the
10 | Software is furnished to do so, subject to the following
11 | conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23 | OTHER DEALINGS IN THE SOFTWARE.
24 |
--------------------------------------------------------------------------------
/pistil/tcp/arbiter.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of pistil released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 | import logging
7 | import os
8 |
9 | from pistil import util
10 | from pistil.pool import PoolArbiter
11 | from pistil.tcp.sock import create_socket
12 |
13 | log = logging.getLogger(__name__)
14 |
15 |
16 | class TcpArbiter(PoolArbiter):
17 |
18 | _LISTENER = None
19 |
20 | def on_init(self, args):
21 | self.address = util.parse_address(args.get('address',
22 | ('127.0.0.1', 8000)))
23 | if not self._LISTENER:
24 | self._LISTENER = create_socket(args)
25 |
26 | # we want to pass the socket to the worker.
27 | self.conf.update({"sock": self._LISTENER})
28 |
29 |
30 | def when_ready(self):
31 | log.info("Listening at: %s (%s)", self._LISTENER,
32 | self.pid)
33 |
34 | def on_reexec(self):
35 | # save the socket file descriptor number in environ to reuse the
36 | # socket after forking a new master.
37 | os.environ['PISTIL_FD'] = str(self._LISTENER.fileno())
38 |
39 | def on_stop(self, graceful=True):
40 | self._LISTENER = None
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/pistil/workertmp.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of pistil released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 | import os
7 | import tempfile
8 |
9 | from pistil import util
10 |
11 | class WorkerTmp(object):
12 |
13 | def __init__(self, cfg):
14 | old_umask = os.umask(cfg.get("umask", 0))
15 | fd, name = tempfile.mkstemp(prefix="wgunicorn-")
16 |
17 | # allows the process to write to the file
18 | util.chown(name, cfg.get("uid", os.geteuid()), cfg.get("gid",
19 | os.getegid()))
20 | os.umask(old_umask)
21 |
22 | # unlink the file so we don't leak tempory files
23 | try:
24 | os.unlink(name)
25 | self._tmp = os.fdopen(fd, 'w+b', 1)
26 | except:
27 | os.close(fd)
28 | raise
29 |
30 | self.spinner = 0
31 |
32 | def notify(self):
33 | try:
34 | self.spinner = (self.spinner+1) % 2
35 | os.fchmod(self._tmp.fileno(), self.spinner)
36 | except AttributeError:
37 | # python < 2.6
38 | self._tmp.truncate(0)
39 | os.write(self._tmp.fileno(), "X")
40 |
41 | def fileno(self):
42 | return self._tmp.fileno()
43 |
44 | def close(self):
45 | return self._tmp.close()
46 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of gunicorn released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 |
7 | import os
8 | from setuptools import setup, find_packages
9 | import sys
10 |
11 | from pistil import __version__
12 |
13 | setup(
14 | name = 'pistil',
15 | version = __version__,
16 |
17 | description = 'Multiprocessing toolkit',
18 | long_description = file(
19 | os.path.join(
20 | os.path.dirname(__file__),
21 | 'README.rst'
22 | )
23 | ).read(),
24 | author = 'Benoit Chesneau',
25 | author_email = 'benoitc@e-engura.com',
26 | license = 'MIT',
27 | url = 'http://github.com/meebo/pistil',
28 |
29 | classifiers = [
30 | 'Development Status :: 4 - Beta',
31 | 'Environment :: Other Environment',
32 | 'Intended Audience :: Developers',
33 | 'License :: OSI Approved :: MIT License',
34 | 'Operating System :: MacOS :: MacOS X',
35 | 'Operating System :: POSIX',
36 | 'Programming Language :: Python',
37 | 'Topic :: Internet',
38 | 'Topic :: Utilities',
39 | 'Topic :: Software Development :: Libraries :: Python Modules',
40 | ],
41 | zip_safe = False,
42 | packages = find_packages(exclude=['examples', 'tests']),
43 | include_package_data = True
44 | )
45 |
--------------------------------------------------------------------------------
/examples/multiworker2.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of pistil released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 | import time
7 | import urllib2
8 |
9 | from pistil.arbiter import Arbiter
10 | from pistil.worker import Worker
11 | from pistil.tcp.sync_worker import TcpSyncWorker
12 | from pistil.tcp.arbiter import TcpArbiter
13 |
14 | from http_parser.http import HttpStream
15 | from http_parser.reader import SocketReader
16 |
17 | class MyTcpWorker(TcpSyncWorker):
18 |
19 | def handle(self, sock, addr):
20 | p = HttpStream(SocketReader(sock))
21 |
22 | path = p.path()
23 | data = "welcome wold"
24 | sock.send("".join(["HTTP/1.1 200 OK\r\n",
25 | "Content-Type: text/html\r\n",
26 | "Content-Length:" + str(len(data)) + "\r\n",
27 | "Connection: close\r\n\r\n",
28 | data]))
29 |
30 |
31 | class UrlWorker(Worker):
32 |
33 | def run(self):
34 | print "ici"
35 | while self.alive:
36 | time.sleep(0.1)
37 | f = urllib2.urlopen("http://localhost:5000")
38 | print f.read()
39 | self.notify()
40 |
41 | class MyPoolArbiter(TcpArbiter):
42 |
43 | def on_init(self, conf):
44 | TcpArbiter.on_init(self, conf)
45 | # we return a spec
46 | return (MyTcpWorker, 30, "worker", {}, "http_welcome",)
47 |
48 |
49 | if __name__ == '__main__':
50 | conf = {"num_workers": 3, "address": ("127.0.0.1", 5000)}
51 |
52 | specs = [
53 | (MyPoolArbiter, 30, "supervisor", {}, "tcp_pool"),
54 | (UrlWorker, 30, "worker", {}, "grabber")
55 | ]
56 |
57 | arbiter = Arbiter(conf, specs)
58 | arbiter.run()
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/pistil/tcp/sync_worker.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of pistil released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 | import errno
7 | import logging
8 | import os
9 | import select
10 | import socket
11 |
12 |
13 | from pistil import util
14 | from pistil.worker import Worker
15 |
16 |
17 | log = logging.getLogger(__name__)
18 |
19 |
20 | class TcpSyncWorker(Worker):
21 |
22 | def on_init_process(self):
23 | self.socket = self.conf.get('sock')
24 | self.address = self.socket.getsockname()
25 | util.close_on_exec(self.socket)
26 |
27 | def run(self):
28 | self.socket.setblocking(0)
29 |
30 | while self.alive:
31 | self.notify()
32 |
33 | # Accept a connection. If we get an error telling us
34 | # that no connection is waiting we fall down to the
35 | # select which is where we'll wait for a bit for new
36 | # workers to come give us some love.
37 | try:
38 | client, addr = self.socket.accept()
39 | client.setblocking(1)
40 | util.close_on_exec(client)
41 | self.handle(client, addr)
42 |
43 | # Keep processing clients until no one is waiting. This
44 | # prevents the need to select() for every client that we
45 | # process.
46 | continue
47 |
48 | except socket.error, e:
49 | if e[0] not in (errno.EAGAIN, errno.ECONNABORTED):
50 | raise
51 |
52 | # If our parent changed then we shut down.
53 | if self.ppid != os.getppid():
54 | log.info("Parent changed, shutting down: %s", self)
55 | return
56 |
57 | try:
58 | self.notify()
59 | ret = select.select([self.socket], [], self._PIPE,
60 | self.timeout / 2.0)
61 | if ret[0]:
62 | continue
63 | except select.error, e:
64 | if e[0] == errno.EINTR:
65 | continue
66 | if e[0] == errno.EBADF:
67 | if self.nr < 0:
68 | continue
69 | else:
70 | return
71 | raise
72 |
73 | def handle(self, client, addr):
74 | raise NotImplementedError
75 |
--------------------------------------------------------------------------------
/examples/_sendfile.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of gunicorn released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 | import errno
7 | import os
8 | import sys
9 |
10 | try:
11 | import ctypes
12 | import ctypes.util
13 | except MemoryError:
14 | # selinux execmem denial
15 | # https://bugzilla.redhat.com/show_bug.cgi?id=488396
16 | raise ImportError
17 |
18 | SUPPORTED_PLATFORMS = (
19 | 'darwin',
20 | 'freebsd',
21 | 'dragonfly',
22 | 'linux2')
23 |
24 | if sys.version_info < (2, 6) or \
25 | sys.platform not in SUPPORTED_PLATFORMS:
26 | raise ImportError("sendfile isn't supported on this platform")
27 |
28 | _libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
29 | _sendfile = _libc.sendfile
30 |
31 | def sendfile(fdout, fdin, offset, nbytes):
32 | if sys.platform == 'darwin':
33 | _sendfile.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_uint64,
34 | ctypes.POINTER(ctypes.c_uint64), ctypes.c_voidp,
35 | ctypes.c_int]
36 | _nbytes = ctypes.c_uint64(nbytes)
37 | result = _sendfile(fdin, fdout, offset, _nbytes, None, 0)
38 |
39 | if result == -1:
40 | e = ctypes.get_errno()
41 | if e == errno.EAGAIN and _nbytes.value:
42 | return nbytes.value
43 | raise OSError(e, os.strerror(e))
44 | return _nbytes.value
45 | elif sys.platform in ('freebsd', 'dragonfly',):
46 | _sendfile.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_uint64,
47 | ctypes.c_uint64, ctypes.c_voidp,
48 | ctypes.POINTER(ctypes.c_uint64), ctypes.c_int]
49 | _sbytes = ctypes.c_uint64()
50 | result = _sendfile(fdin, fdout, offset, nbytes, None, _sbytes, 0)
51 | if result == -1:
52 | e = ctypes.get_errno()
53 | if e == errno.EAGAIN and _sbytes.value:
54 | return _sbytes.value
55 | raise OSError(e, os.strerror(e))
56 | return _sbytes.value
57 |
58 | else:
59 | _sendfile.argtypes = [ctypes.c_int, ctypes.c_int,
60 | ctypes.POINTER(ctypes.c_uint64), ctypes.c_size_t]
61 |
62 | _offset = ctypes.c_uint64(offset)
63 | sent = _sendfile(fdout, fdin, _offset, nbytes)
64 | if sent == -1:
65 | e = ctypes.get_errno()
66 | raise OSError(e, os.strerror(e))
67 | return sent
68 |
69 |
--------------------------------------------------------------------------------
/pistil/tcp/gevent_worker.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of pistil released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 | from __future__ import with_statement
7 |
8 | import os
9 | import sys
10 | import logging
11 | log = logging.getLogger(__name__)
12 |
13 | try:
14 | import gevent
15 | except ImportError:
16 | raise RuntimeError("You need gevent installed to use this worker.")
17 |
18 |
19 | from gevent.pool import Pool
20 | from gevent.server import StreamServer
21 |
22 | from pistil import util
23 | from pistil.tcp.sync_worker import TcpSyncWorker
24 |
25 | # workaround on osx, disable kqueue
26 | if sys.platform == "darwin":
27 | os.environ['EVENT_NOKQUEUE'] = "1"
28 |
29 |
30 | class PStreamServer(StreamServer):
31 | def __init__(self, listener, handle, spawn='default', worker=None):
32 | StreamServer.__init__(self, listener, spawn=spawn)
33 | self.handle_func = handle
34 | self.worker = worker
35 |
36 | def stop(self, timeout=None):
37 | super(PStreamServer, self).stop(timeout=timeout)
38 |
39 | def handle(self, sock, addr):
40 | self.handle_func(sock, addr)
41 |
42 |
43 | class TcpGeventWorker(TcpSyncWorker):
44 |
45 | def on_init(self, conf):
46 | self.worker_connections = conf.get("worker_connections",
47 | 10000)
48 | self.pool = Pool(self.worker_connections)
49 |
50 | def run(self):
51 | self.socket.setblocking(1)
52 |
53 | # start gevent stream server
54 | server = PStreamServer(self.socket, self.handle, spawn=self.pool,
55 | worker=self)
56 | server.start()
57 |
58 | try:
59 | while self.alive:
60 | self.notify()
61 | if self.ppid != os.getppid():
62 | log.info("Parent changed, shutting down: %s", self)
63 | break
64 |
65 | gevent.sleep(1.0)
66 |
67 | except KeyboardInterrupt:
68 | pass
69 |
70 | try:
71 | # Try to stop connections until timeout
72 | self.notify()
73 | server.stop(timeout=self.timeout)
74 | except:
75 | pass
76 |
77 |
78 | if hasattr(gevent.core, 'dns_shutdown'):
79 |
80 | def init_process(self):
81 | #gevent 0.13 and older doesn't reinitialize dns for us after forking
82 | #here's the workaround
83 | gevent.core.dns_shutdown(fail_requests=1)
84 | gevent.core.dns_init()
85 | super(TcpGeventWorker, self).init_process()
86 |
87 |
--------------------------------------------------------------------------------
/pistil/pidfile.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of pistil released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 | from __future__ import with_statement
7 |
8 | import errno
9 | import os
10 | import tempfile
11 |
12 |
13 | class Pidfile(object):
14 | """\
15 | Manage a PID file. If a specific name is provided
16 | it and '"%s.oldpid" % name' will be used. Otherwise
17 | we create a temp file using os.mkstemp.
18 | """
19 |
20 | def __init__(self, fname):
21 | self.fname = fname
22 | self.pid = None
23 |
24 | def create(self, pid):
25 | oldpid = self.validate()
26 | if oldpid:
27 | if oldpid == os.getpid():
28 | return
29 | raise RuntimeError("Already running on PID %s " \
30 | "(or pid file '%s' is stale)" % (os.getpid(), self.fname))
31 |
32 | self.pid = pid
33 |
34 | # Write pidfile
35 | fdir = os.path.dirname(self.fname)
36 | if fdir and not os.path.isdir(fdir):
37 | raise RuntimeError("%s doesn't exist. Can't create pidfile." % fdir)
38 | fd, fname = tempfile.mkstemp(dir=fdir)
39 | os.write(fd, "%s\n" % self.pid)
40 | if self.fname:
41 | os.rename(fname, self.fname)
42 | else:
43 | self.fname = fname
44 | os.close(fd)
45 |
46 | # set permissions to -rw-r--r--
47 | os.chmod(self.fname, 420)
48 |
49 | def rename(self, path):
50 | self.unlink()
51 | self.fname = path
52 | self.create(self.pid)
53 |
54 | def unlink(self):
55 | """ delete pidfile"""
56 | try:
57 | with open(self.fname, "r") as f:
58 | pid1 = int(f.read() or 0)
59 |
60 | if pid1 == self.pid:
61 | os.unlink(self.fname)
62 | except:
63 | pass
64 |
65 | def validate(self):
66 | """ Validate pidfile and make it stale if needed"""
67 | if not self.fname:
68 | return
69 | try:
70 | with open(self.fname, "r") as f:
71 | wpid = int(f.read() or 0)
72 |
73 | if wpid <= 0:
74 | return
75 |
76 | try:
77 | os.kill(wpid, 0)
78 | return wpid
79 | except OSError, e:
80 | if e[0] == errno.ESRCH:
81 | return
82 | raise
83 | except IOError, e:
84 | if e[0] == errno.ENOENT:
85 | return
86 | raise
87 |
88 |
--------------------------------------------------------------------------------
/examples/amqp.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | """
3 | A simple worker with a AMQP consumer.
4 |
5 | This example shows how to implement a simple AMQP consumer
6 | based on `Kombu `_ and shows you
7 | what different kind of workers you can put to a arbiter
8 | to manage the worker lifetime, event handling and shutdown/reload szenarios.
9 | """
10 | import sys
11 | import time
12 | import socket
13 | import logging
14 | from pistil.arbiter import Arbiter
15 | from pistil.worker import Worker
16 | from kombu.connection import BrokerConnection
17 | from kombu.messaging import Exchange, Queue, Consumer, Producer
18 |
19 |
20 | CONNECTION = ('localhost', 'guest', 'default', '/')
21 |
22 | log = logging.getLogger(__name__)
23 |
24 |
25 | class AMQPWorker(Worker):
26 |
27 | queues = [
28 | {'routing_key': 'test',
29 | 'name': 'test',
30 | 'handler': 'handle_test'
31 | }
32 | ]
33 |
34 | _connection = None
35 |
36 | def handle_test(self, body, message):
37 | log.debug("Handle message: %s" % body)
38 | message.ack()
39 |
40 | def handle(self):
41 | log.debug("Start consuming")
42 | exchange = Exchange('amqp.topic', type='direct', durable=True)
43 | self._connection = BrokerConnection(*CONNECTION)
44 | channel = self._connection.channel()
45 |
46 | for entry in self.queues:
47 | log.debug("prepare to consume %s" % entry['routing_key'])
48 | queue = Queue(entry['name'], exchange=exchange,
49 | routing_key=entry['routing_key'])
50 | consumer = Consumer(channel, queue)
51 | consumer.register_callback(getattr(self, entry['handler']))
52 | consumer.consume()
53 |
54 | log.debug("start consuming...")
55 | while True:
56 | try:
57 | self._connection.drain_events()
58 | except socket.timeout:
59 | log.debug("nothing to consume...")
60 | break
61 | self._connection.close()
62 |
63 | def run(self):
64 | while self.alive:
65 | try:
66 | self.handle()
67 | except Exception:
68 | self.alive = False
69 | raise
70 |
71 | def handle_quit(self, sig, frame):
72 | if self._connection is not None:
73 | self._connection.close()
74 | self.alive = False
75 |
76 | def handle_exit(self, sig, frame):
77 | if self._connection is not None:
78 | self._connection.close()
79 | self.alive = False
80 | sys.exit(0)
81 |
82 |
83 | if __name__ == "__main__":
84 | conf = {}
85 | specs = [(AMQPWorker, None, "worker", {}, "test")]
86 | a = Arbiter(conf, specs)
87 | a.run()
88 |
--------------------------------------------------------------------------------
/pistil/worker.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of pistil released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 | from __future__ import with_statement
7 |
8 | import logging
9 | import os
10 | import signal
11 | import sys
12 | import time
13 | import traceback
14 |
15 |
16 | from pistil import util
17 | from pistil.workertmp import WorkerTmp
18 |
19 | log = logging.getLogger(__name__)
20 |
21 | class Worker(object):
22 |
23 | _SIGNALS = map(
24 | lambda x: getattr(signal, "SIG%s" % x),
25 | "HUP QUIT INT TERM USR1 USR2 WINCH CHLD".split()
26 | )
27 |
28 | _PIPE = []
29 |
30 |
31 | def __init__(self, conf, name=None, child_type="worker",
32 | age=0, ppid=0, timeout=30):
33 |
34 | if name is None:
35 | name = self.__class__.__name__
36 | self.name = name
37 |
38 | self.child_type = child_type
39 | self.age = age
40 | self.ppid = ppid
41 | self.timeout = timeout
42 | self.conf = conf
43 |
44 |
45 | # initialize
46 | self.booted = False
47 | self.alive = True
48 | self.debug =self.conf.get("debug", False)
49 | self.tmp = WorkerTmp(self.conf)
50 |
51 | self.on_init(self.conf)
52 |
53 | def on_init(self, conf):
54 | pass
55 |
56 |
57 | def pid(self):
58 | return os.getpid()
59 | pid = util.cached_property(pid)
60 |
61 | def notify(self):
62 | """\
63 | Your worker subclass must arrange to have this method called
64 | once every ``self.timeout`` seconds. If you fail in accomplishing
65 | this task, the master process will murder your workers.
66 | """
67 | self.tmp.notify()
68 |
69 |
70 | def handle(self):
71 | raise NotImplementedError
72 |
73 | def run(self):
74 | """\
75 | This is the mainloop of a worker process. You should override
76 | this method in a subclass to provide the intended behaviour
77 | for your particular evil schemes.
78 | """
79 | while True:
80 | self.notify()
81 | self.handle()
82 | time.sleep(0.1)
83 |
84 | def on_init_process(self):
85 | """ method executed when we init a process """
86 | pass
87 |
88 | def init_process(self):
89 | """\
90 | If you override this method in a subclass, the last statement
91 | in the function should be to call this method with
92 | super(MyWorkerClass, self).init_process() so that the ``run()``
93 | loop is initiated.
94 | """
95 | util.set_owner_process(self.conf.get("uid", os.geteuid()),
96 | self.conf.get("gid", os.getegid()))
97 |
98 | # Reseed the random number generator
99 | util.seed()
100 |
101 | # For waking ourselves up
102 | self._PIPE = os.pipe()
103 | map(util.set_non_blocking, self._PIPE)
104 | map(util.close_on_exec, self._PIPE)
105 |
106 | # Prevent fd inherientence
107 | util.close_on_exec(self.tmp.fileno())
108 | self.init_signals()
109 |
110 | self.on_init_process()
111 |
112 | # Enter main run loop
113 | self.booted = True
114 | self.run()
115 |
116 | def init_signals(self):
117 | map(lambda s: signal.signal(s, signal.SIG_DFL), self._SIGNALS)
118 | signal.signal(signal.SIGQUIT, self.handle_quit)
119 | signal.signal(signal.SIGTERM, self.handle_exit)
120 | signal.signal(signal.SIGINT, self.handle_exit)
121 | signal.signal(signal.SIGWINCH, self.handle_winch)
122 |
123 | def handle_quit(self, sig, frame):
124 | self.alive = False
125 |
126 | def handle_exit(self, sig, frame):
127 | self.alive = False
128 | sys.exit(0)
129 |
130 | def handle_winch(self, sig, fname):
131 | # Ignore SIGWINCH in worker. Fixes a crash on OpenBSD.
132 | return
133 |
--------------------------------------------------------------------------------
/pistil/tcp/sock.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of gunicorn released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 | import errno
7 | import logging
8 | import os
9 | import socket
10 | import sys
11 | import time
12 |
13 | from pistil import util
14 |
15 | log = logging.getLogger(__name__)
16 |
17 | class BaseSocket(object):
18 |
19 | def __init__(self, conf, fd=None):
20 | self.conf = conf
21 | self.address = util.parse_address(conf.get('address',
22 | ('127.0.0.1', 8000)))
23 | if fd is None:
24 | sock = socket.socket(self.FAMILY, socket.SOCK_STREAM)
25 | else:
26 | sock = socket.fromfd(fd, self.FAMILY, socket.SOCK_STREAM)
27 | self.sock = self.set_options(sock, bound=(fd is not None))
28 |
29 | def __str__(self, name):
30 | return "" % self.sock.fileno()
31 |
32 | def __getattr__(self, name):
33 | return getattr(self.sock, name)
34 |
35 | def set_options(self, sock, bound=False):
36 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
37 | if not bound:
38 | self.bind(sock)
39 | sock.setblocking(0)
40 | sock.listen(self.conf.get('backlog', 2048))
41 | return sock
42 |
43 | def bind(self, sock):
44 | sock.bind(self.address)
45 |
46 | def close(self):
47 | try:
48 | self.sock.close()
49 | except socket.error, e:
50 | log.info("Error while closing socket %s", str(e))
51 | time.sleep(0.3)
52 | del self.sock
53 |
54 | class TCPSocket(BaseSocket):
55 |
56 | FAMILY = socket.AF_INET
57 |
58 | def __str__(self):
59 | return "http://%s:%d" % self.sock.getsockname()
60 |
61 | def set_options(self, sock, bound=False):
62 | sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
63 | return super(TCPSocket, self).set_options(sock, bound=bound)
64 |
65 | class TCP6Socket(TCPSocket):
66 |
67 | FAMILY = socket.AF_INET6
68 |
69 | def __str__(self):
70 | (host, port, fl, sc) = self.sock.getsockname()
71 | return "http://[%s]:%d" % (host, port)
72 |
73 | class UnixSocket(BaseSocket):
74 |
75 | FAMILY = socket.AF_UNIX
76 |
77 | def __init__(self, conf, fd=None):
78 | if fd is None:
79 | try:
80 | os.remove(conf.address)
81 | except OSError:
82 | pass
83 | super(UnixSocket, self).__init__(conf, fd=fd)
84 |
85 | def __str__(self):
86 | return "unix:%s" % self.address
87 |
88 | def bind(self, sock):
89 | old_umask = os.umask(self.conf.get("umask", 0))
90 | sock.bind(self.address)
91 | util.chown(self.address, self.conf.get("uid", os.geteuid()),
92 | self.conf.get("gid", os.getegid()))
93 | os.umask(old_umask)
94 |
95 | def close(self):
96 | super(UnixSocket, self).close()
97 | os.unlink(self.address)
98 |
99 | def create_socket(conf):
100 | """
101 | Create a new socket for the given address. If the
102 | address is a tuple, a TCP socket is created. If it
103 | is a string, a Unix socket is created. Otherwise
104 | a TypeError is raised.
105 | """
106 | # get it only once
107 | addr = conf.get("address", ('127.0.0.1', 8000))
108 |
109 | if isinstance(addr, tuple):
110 | if util.is_ipv6(addr[0]):
111 | sock_type = TCP6Socket
112 | else:
113 | sock_type = TCPSocket
114 | elif isinstance(addr, basestring):
115 | sock_type = UnixSocket
116 | else:
117 | raise TypeError("Unable to create socket from: %r" % addr)
118 |
119 | if 'PISTIL_FD' in os.environ:
120 | fd = int(os.environ.pop('PISTIL_FD'))
121 | try:
122 | return sock_type(conf, fd=fd)
123 | except socket.error, e:
124 | if e[0] == errno.ENOTCONN:
125 | log.error("PISTIL_FD should refer to an open socket.")
126 | else:
127 | raise
128 |
129 | # If we fail to create a socket from GUNICORN_FD
130 | # we fall through and try and open the socket
131 | # normally.
132 |
133 | for i in range(5):
134 | try:
135 | return sock_type(conf)
136 | except socket.error, e:
137 | if e[0] == errno.EADDRINUSE:
138 | log.error("Connection in use: %s", str(addr))
139 | if e[0] == errno.EADDRNOTAVAIL:
140 | log.error("Invalid address: %s", str(addr))
141 | sys.exit(1)
142 | if i < 5:
143 | log.error("Retrying in 1 second.")
144 | time.sleep(1)
145 |
146 | log.error("Can't connect to %s", str(addr))
147 | sys.exit(1)
148 |
149 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | pistil
2 | ------
3 |
4 |
5 | Simple multiprocessing toolkit. This is based on the `Gunicorn `_ multiprocessing engine.
6 |
7 | This library allows you to supervise multiple type of workers and chain
8 | supervisors. Gracefull, reload, signaling between workers is handled.
9 |
10 |
11 | Simple Arbiter launching one worker::
12 |
13 | from pistil.arbiter import Arbiter
14 | from pistil.worker import Worker
15 |
16 | class MyWorker(Worker):
17 |
18 | def handle(self):
19 | print "hello from worker n°%s" % self.pid
20 |
21 | if __name__ == "__main__":
22 | conf = {}
23 | specs = [(MyWorker, 30, "worker", {}, "test")]
24 | a = Arbiter(conf, specs)
25 | a.run()
26 |
27 | The same with different with the Pool arbiter. This time we send the
28 | same worker on 3 os processes::
29 |
30 | from pistil.pool import PoolArbiter
31 | from pistil.worker import Worker
32 |
33 | class MyWorker(Worker):
34 |
35 | def handle(self):
36 | print "hello from worker n°%s" % self.pid
37 |
38 | if __name__ == "__main__":
39 | conf = {"num_workers": 3 }
40 | spec = (MyWorker, 30, "worker", {}, "test",)
41 | a = PoolArbiter(conf, spec)
42 | a.run()
43 |
44 | A common use for that pattern is a tcp server tjhat share the same
45 | socket between them. For that purpose pistil provides the TcpArbiter and
46 | TcpSyncWorker and the GeventTcpWorker to use with gevent.
47 |
48 | Pistil allows you to mix diffrent kind of workers in an arbiter::
49 |
50 | from pistil.arbiter import Arbiter
51 | from pistil.worker import Worker
52 |
53 | class MyWorker(Worker):
54 |
55 | def handle(self):
56 | print "hello worker 1 from %s" % self.name
57 |
58 | class MyWorker2(Worker):
59 |
60 | def handle(self):
61 | print "hello worker 2 from %s" % self.name
62 |
63 |
64 | if __name__ == '__main__':
65 | conf = {}
66 |
67 | specs = [
68 | (MyWorker, 30, "worker", {}, "w1"),
69 | (MyWorker2, 30, "worker", {}, "w2"),
70 | (MyWorker2, 30, "kill", {}, "w3")
71 | ]
72 | # launchh the arbiter
73 | arbiter = Arbiter(conf, specs)
74 | arbiter.run()
75 |
76 | You can also chain arbiters::
77 |
78 | import time
79 | import urllib2
80 |
81 | from pistil.arbiter import Arbiter
82 | from pistil.worker import Worker
83 | from pistil.tcp.sync_worker import TcpSyncWorker
84 | from pistil.tcp.arbiter import TcpArbiter
85 |
86 | from http_parser.http import HttpStream
87 | from http_parser.reader import SocketReader
88 |
89 | class MyTcpWorker(TcpSyncWorker):
90 |
91 | def handle(self, sock, addr):
92 | p = HttpStream(SocketReader(sock))
93 | path = p.path()
94 | data = "welcome wold"
95 | sock.send("".join(["HTTP/1.1 200 OK\r\n",
96 | "Content-Type: text/html\r\n",
97 | "Content-Length:" + str(len(data)) + "\r\n",
98 | "Connection: close\r\n\r\n",
99 | data]))
100 |
101 |
102 | class UrlWorker(Worker):
103 |
104 | def handle(self):
105 | f = urllib2.urlopen("http://localhost:5000")
106 | print f.read()
107 |
108 | class MyPoolArbiter(TcpArbiter):
109 |
110 | def on_init(self, conf):
111 | TcpArbiter.on_init(self, conf)
112 | # we return a spec
113 | return (MyTcpWorker, 30, "worker", {}, "http_welcome",)
114 |
115 |
116 | if __name__ == '__main__':
117 | conf = {"num_workers": 3, "address": ("127.0.0.1", 5000)}
118 |
119 | specs = [
120 | (MyPoolArbiter, 30, "supervisor", {}, "tcp_pool"),
121 | (UrlWorker, 30, "worker", {}, "grabber")
122 | ]
123 |
124 | arbiter = Arbiter(conf, specs)
125 | arbiter.run()
126 |
127 |
128 | This examplelaunch a web server with 3 workers on port 5000 and another
129 | worker fetching the welcome page hosted by this server::
130 |
131 |
132 | $ python examples/multiworker2.py
133 |
134 | 2011-08-08 00:05:42 [13195] [DEBUG] Arbiter master booted on 13195
135 | 2011-08-08 00:05:42 [13196] [INFO] Booting grabber (worker) with pid: 13196
136 | ici
137 | 2011-08-08 00:05:42 [13197] [INFO] Booting pool (supervisor) with pid: 13197
138 | 2011-08-08 00:05:42 [13197] [DEBUG] Arbiter pool booted on 13197
139 | 2011-08-08 00:05:42 [13197] [INFO] Listening at: http://127.0.0.1:5000 (13197)
140 | 2011-08-08 00:05:42 [13198] [INFO] Booting worker (worker) with pid: 13198
141 | 2011-08-08 00:05:42 [13199] [INFO] Booting worker (worker) with pid: 13199
142 | welcome world
143 | welcome world
144 |
145 |
146 | More documentation is comming. See also examples in the examples/
147 | folder.
148 |
--------------------------------------------------------------------------------
/examples/serve_file.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of gunicorn released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 | import mimetypes
7 | import os
8 |
9 |
10 | from gevent import monkey
11 | monkey.noisy = False
12 | monkey.patch_all()
13 |
14 |
15 | from http_parser.http import HttpStream
16 | from http_parser.reader import SocketReader
17 |
18 | from pistil import util
19 | from pistil.tcp.arbiter import TcpArbiter
20 | from pistil.tcp.gevent_worker import TcpGeventWorker
21 |
22 | CURDIR = os.path.dirname(__file__)
23 |
24 | try:
25 | # Python 3.3 has os.sendfile().
26 | from os import sendfile
27 | except ImportError:
28 | try:
29 | from _sendfile import sendfile
30 | except ImportError:
31 | sendfile = None
32 |
33 | def write_error(sock, status_int, reason, mesg):
34 | html = textwrap.dedent("""\
35 |
36 |
37 | %(reason)s
38 |
39 |
40 | %(reason)s
41 | %(mesg)s
42 |
43 |
44 | """) % {"reason": reason, "mesg": mesg}
45 |
46 | http = textwrap.dedent("""\
47 | HTTP/1.1 %s %s\r
48 | Connection: close\r
49 | Content-Type: text/html\r
50 | Content-Length: %d\r
51 | \r
52 | %s
53 | """) % (str(status_int), reason, len(html), html)
54 | write_nonblock(sock, http)
55 |
56 |
57 |
58 | class HttpWorker(TcpGeventWorker):
59 |
60 | def handle(self, sock, addr):
61 | p = HttpStream(SocketReader(sock))
62 |
63 | path = p.path()
64 |
65 | if not path or path == "/":
66 | path = "index.html"
67 |
68 | if path.startswith("/"):
69 | path = path[1:]
70 |
71 | real_path = os.path.join(CURDIR, "static", path)
72 |
73 | if os.path.isdir(real_path):
74 | lines = [""]
75 | for d in os.listdir(real_path):
76 | fpath = os.path.join(real_path, d)
77 | lines.append("- " + d + "")
78 |
79 | data = "".join(lines)
80 | resp = "".join(["HTTP/1.1 200 OK\r\n",
81 | "Content-Type: text/html\r\n",
82 | "Content-Length:" + str(len(data)) + "\r\n",
83 | "Connection: close\r\n\r\n",
84 | data])
85 | sock.sendall(resp)
86 |
87 | elif not os.path.exists(real_path):
88 | util.write_error(sock, 404, "Not found", real_path + " not found")
89 | else:
90 | ctype = mimetypes.guess_type(real_path)[0]
91 |
92 | if ctype.startswith('text') or 'html' in ctype:
93 |
94 | try:
95 | f = open(real_path, 'rb')
96 | data = f.read()
97 | resp = "".join(["HTTP/1.1 200 OK\r\n",
98 | "Content-Type: " + ctype + "\r\n",
99 | "Content-Length:" + str(len(data)) + "\r\n",
100 | "Connection: close\r\n\r\n",
101 | data])
102 | sock.sendall(resp)
103 | finally:
104 | f.close()
105 | else:
106 |
107 | try:
108 | f = open(real_path, 'r')
109 | clen = int(os.fstat(f.fileno())[6])
110 |
111 | # send headers
112 | sock.send("".join(["HTTP/1.1 200 OK\r\n",
113 | "Content-Type: " + ctype + "\r\n",
114 | "Content-Length:" + str(clen) + "\r\n",
115 | "Connection: close\r\n\r\n"]))
116 |
117 | if not sendfile:
118 | while True:
119 | data = f.read(4096)
120 | if not data:
121 | break
122 | sock.send(data)
123 | else:
124 | fileno = f.fileno()
125 | sockno = sock.fileno()
126 | sent = 0
127 | offset = 0
128 | nbytes = clen
129 | sent += sendfile(sockno, fileno, offset+sent, nbytes-sent)
130 | while sent != nbytes:
131 | sent += sendfile(sock.fileno(), fileno, offset+sent, nbytes-sent)
132 |
133 |
134 | finally:
135 | f.close()
136 |
137 |
138 | def main():
139 | conf = {"address": ("127.0.0.1", 5000), "debug": True,
140 | "num_workers": 3}
141 | spec = (HttpWorker, 30, "send_file", {}, "worker",)
142 |
143 | arbiter = TcpArbiter(conf, spec)
144 | arbiter.run()
145 |
146 |
147 | if __name__ == '__main__':
148 | main()
149 |
--------------------------------------------------------------------------------
/pistil/pool.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of pistil released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 | import errno
7 | import os
8 | import signal
9 |
10 | from pistil.errors import HaltServer
11 | from pistil.arbiter import Arbiter, Child
12 | from pistil.workertmp import WorkerTmp
13 | from pistil import util
14 |
15 | DEFAULT_CONF = dict(
16 | uid = os.geteuid(),
17 | gid = os.getegid(),
18 | umask = 0,
19 | debug = False,
20 | num_workers = 1,
21 | )
22 |
23 |
24 | class PoolArbiter(Arbiter):
25 |
26 |
27 | _SIGNALS = map(
28 | lambda x: getattr(signal, "SIG%s" % x),
29 | "HUP QUIT INT TERM TTIN TTOU USR1 USR2 WINCH".split()
30 | )
31 |
32 | def __init__(self, args, spec=(), name=None,
33 | child_type="supervisor", age=0, ppid=0,
34 | timeout=30):
35 |
36 | if not isinstance(spec, tuple):
37 | raise TypeError("spec should be a tuple")
38 |
39 | # set conf
40 | conf = DEFAULT_CONF.copy()
41 | conf.update(args)
42 | self.conf = conf
43 |
44 | # set number of workers
45 | self.num_workers = conf.get('num_workers', 1)
46 |
47 | ret = self.on_init(conf)
48 | if not ret:
49 | self._SPEC = Child(*spec)
50 | else:
51 | self._SPEC = Child(*ret)
52 |
53 | if name is None:
54 | name = self.__class__.__name__
55 |
56 | self.name = name
57 | self.child_type = child_type
58 | self.age = age
59 | self.ppid = ppid
60 | self.timeout = timeout
61 |
62 |
63 | self.pid = None
64 | self.child_age = 0
65 | self.booted = False
66 | self.stopping = False
67 | self.debug =self.conf.get("debug", False)
68 | self.tmp = WorkerTmp(self.conf)
69 |
70 | def update_proc_title(self):
71 | util._setproctitle("arbiter [%s running %s workers]" % (self.name,
72 | self.num_workers))
73 |
74 | def on_init(self, conf):
75 | return None
76 |
77 | def on_init_process(self):
78 | self.update_proc_title()
79 |
80 | def handle_ttin(self):
81 | """\
82 | SIGTTIN handling.
83 | Increases the number of workers by one.
84 | """
85 | self.num_workers += 1
86 | self.update_proc_title()
87 | self.manage_workers()
88 |
89 | def handle_ttou(self):
90 | """\
91 | SIGTTOU handling.
92 | Decreases the number of workers by one.
93 | """
94 | if self.num_workers <= 1:
95 | return
96 | self.num_workers -= 1
97 | self.update_proc_title()
98 | self.manage_workers()
99 |
100 | def reload(self):
101 | """
102 | used on HUP
103 | """
104 |
105 | # exec on reload hook
106 | self.on_reload()
107 |
108 | # spawn new workers with new app & conf
109 | for i in range(self.conf.get("num_workers", 1)):
110 | self.spawn_child(self._SPEC)
111 |
112 | # set new proc_name
113 | util._setproctitle("master [%s]" % self.name)
114 |
115 | # manage workers
116 | self.manage_workers()
117 |
118 | def reap_workers(self):
119 | """\
120 | Reap workers to avoid zombie processes
121 | """
122 | try:
123 | while True:
124 | wpid, status = os.waitpid(-1, os.WNOHANG)
125 | if not wpid:
126 | break
127 |
128 | # A worker said it cannot boot. We'll shutdown
129 | # to avoid infinite start/stop cycles.
130 | exitcode = status >> 8
131 | if exitcode == self._WORKER_BOOT_ERROR:
132 | reason = "Worker failed to boot."
133 | raise HaltServer(reason, self._WORKER_BOOT_ERROR)
134 | child_info = self._WORKERS.pop(wpid, None)
135 |
136 | if not child_info:
137 | continue
138 |
139 | child, state = child_info
140 | child.tmp.close()
141 | except OSError, e:
142 | if e.errno == errno.ECHILD:
143 | pass
144 |
145 | def manage_workers(self):
146 | """\
147 | Maintain the number of workers by spawning or killing
148 | as required.
149 | """
150 | if len(self._WORKERS.keys()) < self.num_workers:
151 | self.spawn_workers()
152 |
153 | workers = self._WORKERS.items()
154 | workers.sort(key=lambda w: w[1][0].age)
155 | while len(workers) > self.num_workers:
156 | (pid, _) = workers.pop(0)
157 | self.kill_worker(pid, signal.SIGQUIT)
158 |
159 | def spawn_workers(self):
160 | """\
161 | Spawn new workers as needed.
162 |
163 | This is where a worker process leaves the main loop
164 | of the master process.
165 | """
166 | for i in range(self.num_workers - len(self._WORKERS.keys())):
167 | self.spawn_child(self._SPEC)
168 |
169 |
170 | def kill_worker(self, pid, sig):
171 | """\
172 | Kill a worker
173 |
174 | :attr pid: int, worker pid
175 | :attr sig: `signal.SIG*` value
176 | """
177 | if not isinstance(pid, int):
178 | return
179 |
180 | try:
181 | os.kill(pid, sig)
182 | except OSError, e:
183 | if e.errno == errno.ESRCH:
184 | try:
185 | (child, info) = self._WORKERS.pop(pid)
186 | child.tmp.close()
187 | return
188 | except (KeyError, OSError):
189 | return
190 | raise
191 |
--------------------------------------------------------------------------------
/pistil/util.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of gunicorn released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 |
7 | try:
8 | import ctypes
9 | except MemoryError:
10 | # selinux execmem denial
11 | # https://bugzilla.redhat.com/show_bug.cgi?id=488396
12 | ctypes = None
13 | except ImportError:
14 | # Python on Solaris compiled with Sun Studio doesn't have ctypes
15 | ctypes = None
16 |
17 | import fcntl
18 | import os
19 | import pkg_resources
20 | import random
21 | import resource
22 | import socket
23 | import sys
24 | import textwrap
25 | import time
26 |
27 |
28 | MAXFD = 1024
29 | if (hasattr(os, "devnull")):
30 | REDIRECT_TO = os.devnull
31 | else:
32 | REDIRECT_TO = "/dev/null"
33 |
34 | timeout_default = object()
35 |
36 | CHUNK_SIZE = (16 * 1024)
37 |
38 | MAX_BODY = 1024 * 132
39 |
40 | weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
41 | monthname = [None,
42 | 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
43 | 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
44 |
45 | # Server and Date aren't technically hop-by-hop
46 | # headers, but they are in the purview of the
47 | # origin server which the WSGI spec says we should
48 | # act like. So we drop them and add our own.
49 | #
50 | # In the future, concatenation server header values
51 | # might be better, but nothing else does it and
52 | # dropping them is easier.
53 | hop_headers = set("""
54 | connection keep-alive proxy-authenticate proxy-authorization
55 | te trailers transfer-encoding upgrade
56 | server date
57 | """.split())
58 |
59 | try:
60 | from setproctitle import setproctitle
61 | def _setproctitle(title):
62 | setproctitle(title)
63 | except ImportError:
64 | def _setproctitle(title):
65 | return
66 |
67 | def load_worker_class(uri):
68 | if uri.startswith("egg:"):
69 | # uses entry points
70 | entry_str = uri.split("egg:")[1]
71 | try:
72 | dist, name = entry_str.rsplit("#",1)
73 | except ValueError:
74 | dist = entry_str
75 | name = "sync"
76 |
77 | return pkg_resources.load_entry_point(dist, "gunicorn.workers", name)
78 | else:
79 | components = uri.split('.')
80 | if len(components) == 1:
81 | try:
82 | if uri.startswith("#"):
83 | uri = uri[1:]
84 | return pkg_resources.load_entry_point("gunicorn",
85 | "gunicorn.workers", uri)
86 | except ImportError:
87 | raise RuntimeError("arbiter uri invalid or not found")
88 | klass = components.pop(-1)
89 | mod = __import__('.'.join(components))
90 | for comp in components[1:]:
91 | mod = getattr(mod, comp)
92 | return getattr(mod, klass)
93 |
94 | def set_owner_process(uid,gid):
95 | """ set user and group of workers processes """
96 | if gid:
97 | try:
98 | os.setgid(gid)
99 | except OverflowError:
100 | if not ctypes:
101 | raise
102 | # versions of python < 2.6.2 don't manage unsigned int for
103 | # groups like on osx or fedora
104 | os.setgid(-ctypes.c_int(-gid).value)
105 |
106 | if uid:
107 | os.setuid(uid)
108 |
109 | def chown(path, uid, gid):
110 | try:
111 | os.chown(path, uid, gid)
112 | except OverflowError:
113 | if not ctypes:
114 | raise
115 | os.chown(path, uid, -ctypes.c_int(-gid).value)
116 |
117 |
118 | def is_ipv6(addr):
119 | try:
120 | socket.inet_pton(socket.AF_INET6, addr)
121 | except socket.error: # not a valid address
122 | return False
123 | return True
124 |
125 | def parse_address(netloc, default_port=8000):
126 | if isinstance(netloc, tuple):
127 | return netloc
128 |
129 | if netloc.startswith("unix:"):
130 | return netloc.split("unix:")[1]
131 |
132 | # get host
133 | if '[' in netloc and ']' in netloc:
134 | host = netloc.split(']')[0][1:].lower()
135 | elif ':' in netloc:
136 | host = netloc.split(':')[0].lower()
137 | elif netloc == "":
138 | host = "0.0.0.0"
139 | else:
140 | host = netloc.lower()
141 |
142 | #get port
143 | netloc = netloc.split(']')[-1]
144 | if ":" in netloc:
145 | port = netloc.split(':', 1)[1]
146 | if not port.isdigit():
147 | raise RuntimeError("%r is not a valid port number." % port)
148 | port = int(port)
149 | else:
150 | port = default_port
151 | return (host, port)
152 |
153 | def get_maxfd():
154 | maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
155 | if (maxfd == resource.RLIM_INFINITY):
156 | maxfd = MAXFD
157 | return maxfd
158 |
159 | def close_on_exec(fd):
160 | flags = fcntl.fcntl(fd, fcntl.F_GETFD)
161 | flags |= fcntl.FD_CLOEXEC
162 | fcntl.fcntl(fd, fcntl.F_SETFD, flags)
163 |
164 | def set_non_blocking(fd):
165 | flags = fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK
166 | fcntl.fcntl(fd, fcntl.F_SETFL, flags)
167 |
168 | def close(sock):
169 | try:
170 | sock.close()
171 | except socket.error:
172 | pass
173 |
174 | def write_chunk(sock, data):
175 | chunk = "".join(("%X\r\n" % len(data), data, "\r\n"))
176 | sock.sendall(chunk)
177 |
178 | def write(sock, data, chunked=False):
179 | if chunked:
180 | return write_chunk(sock, data)
181 | sock.sendall(data)
182 |
183 | def write_nonblock(sock, data, chunked=False):
184 | timeout = sock.gettimeout()
185 | if timeout != 0.0:
186 | try:
187 | sock.setblocking(0)
188 | return write(sock, data, chunked)
189 | finally:
190 | sock.setblocking(1)
191 | else:
192 | return write(sock, data, chunked)
193 |
194 | def writelines(sock, lines, chunked=False):
195 | for line in list(lines):
196 | write(sock, line, chunked)
197 |
198 | def write_error(sock, status_int, reason, mesg):
199 | html = textwrap.dedent("""\
200 |
201 |
202 | %(reason)s
203 |
204 |
205 |
%(reason)s
206 | %(mesg)s
207 |
208 |
209 | """) % {"reason": reason, "mesg": mesg}
210 |
211 | http = textwrap.dedent("""\
212 | HTTP/1.1 %s %s\r
213 | Connection: close\r
214 | Content-Type: text/html\r
215 | Content-Length: %d\r
216 | \r
217 | %s
218 | """) % (str(status_int), reason, len(html), html)
219 | write_nonblock(sock, http)
220 |
221 | def normalize_name(name):
222 | return "-".join([w.lower().capitalize() for w in name.split("-")])
223 |
224 | def import_app(module):
225 | parts = module.split(":", 1)
226 | if len(parts) == 1:
227 | module, obj = module, "application"
228 | else:
229 | module, obj = parts[0], parts[1]
230 |
231 | try:
232 | __import__(module)
233 | except ImportError:
234 | if module.endswith(".py") and os.path.exists(module):
235 | raise ImportError("Failed to find application, did "
236 | "you mean '%s:%s'?" % (module.rsplit(".",1)[0], obj))
237 | else:
238 | raise
239 |
240 | mod = sys.modules[module]
241 | app = eval(obj, mod.__dict__)
242 | if app is None:
243 | raise ImportError("Failed to find application object: %r" % obj)
244 | if not callable(app):
245 | raise TypeError("Application object must be callable.")
246 | return app
247 |
248 | def http_date(timestamp=None):
249 | """Return the current date and time formatted for a message header."""
250 | if timestamp is None:
251 | timestamp = time.time()
252 | year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp)
253 | s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
254 | weekdayname[wd],
255 | day, monthname[month], year,
256 | hh, mm, ss)
257 | return s
258 |
259 | def to_bytestring(s):
260 | """ convert to bytestring an unicode """
261 | if not isinstance(s, basestring):
262 | return s
263 | if isinstance(s, unicode):
264 | return s.encode('utf-8')
265 | else:
266 | return s
267 |
268 | def is_hoppish(header):
269 | return header.lower().strip() in hop_headers
270 |
271 | def daemonize():
272 | """\
273 | Standard daemonization of a process.
274 | http://www.svbug.com/documentation/comp.unix.programmer-FAQ/faq_2.html#SEC16
275 | """
276 | if not 'GUNICORN_FD' in os.environ:
277 | if os.fork():
278 | os._exit(0)
279 | os.setsid()
280 |
281 | if os.fork():
282 | os._exit(0)
283 |
284 | os.umask(0)
285 | maxfd = get_maxfd()
286 |
287 | # Iterate through and close all file descriptors.
288 | for fd in range(0, maxfd):
289 | try:
290 | os.close(fd)
291 | except OSError: # ERROR, fd wasn't open to begin with (ignored)
292 | pass
293 |
294 | os.open(REDIRECT_TO, os.O_RDWR)
295 | os.dup2(0, 1)
296 | os.dup2(0, 2)
297 |
298 | def seed():
299 | try:
300 | random.seed(os.urandom(64))
301 | except NotImplementedError:
302 | random.seed(random.random())
303 |
304 |
305 | class _Missing(object):
306 |
307 | def __repr__(self):
308 | return 'no value'
309 |
310 | def __reduce__(self):
311 | return '_missing'
312 |
313 | _missing = _Missing()
314 |
315 | class cached_property(object):
316 |
317 | def __init__(self, func, name=None, doc=None):
318 | self.__name__ = name or func.__name__
319 | self.__module__ = func.__module__
320 | self.__doc__ = doc or func.__doc__
321 | self.func = func
322 |
323 | def __get__(self, obj, type=None):
324 | if obj is None:
325 | return self
326 | value = obj.__dict__.get(self.__name__, _missing)
327 | if value is _missing:
328 | value = self.func(obj)
329 | obj.__dict__[self.__name__] = value
330 | return value
331 |
--------------------------------------------------------------------------------
/pistil/arbiter.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | #
3 | # This file is part of pistil released under the MIT license.
4 | # See the NOTICE for more information.
5 |
6 | from __future__ import with_statement
7 |
8 | import errno
9 | import logging
10 | import os
11 | import select
12 | import signal
13 | import sys
14 | import time
15 | import traceback
16 |
17 | from pistil.errors import HaltServer
18 | from pistil.workertmp import WorkerTmp
19 | from pistil import util
20 | from pistil import __version__, SERVER_SOFTWARE
21 |
22 | LOG_LEVELS = {
23 | "critical": logging.CRITICAL,
24 | "error": logging.ERROR,
25 | "warning": logging.WARNING,
26 | "info": logging.INFO,
27 | "debug": logging.DEBUG
28 | }
29 |
30 | DEFAULT_CONF = dict(
31 | uid = os.geteuid(),
32 | gid = os.getegid(),
33 | umask = 0,
34 | debug = False,
35 | )
36 |
37 |
38 | RESTART_WORKERS = ("worker", "supervisor")
39 |
40 | log = logging.getLogger(__name__)
41 |
42 |
43 | logging.basicConfig(format="%(asctime)s [%(process)d] [%(levelname)s] %(message)s",
44 | datefmt="%Y-%m-%d %H:%M:%S", level=logging.DEBUG)
45 |
46 |
47 |
48 |
49 | class Child(object):
50 |
51 | def __init__(self, child_class, timeout, child_type,
52 | args, name):
53 | self.child_class= child_class
54 | self.timeout = timeout
55 | self.child_type = child_type
56 | self.args = args
57 | self.name = name
58 |
59 |
60 | # chaine init worker:
61 | # (WorkerClass, max_requests, timeout, type, args, name)
62 | # types: supervisor, kill, brutal_kill, worker
63 | # timeout: integer in seconds or None
64 |
65 | class Arbiter(object):
66 | """
67 | Arbiter maintain the workers processes alive. It launches or
68 | kills them if needed. It also manages application reloading
69 | via SIGHUP/USR2.
70 | """
71 |
72 | _SPECS_BYNAME = {}
73 | _CHILDREN_SPECS = []
74 |
75 | # A flag indicating if a worker failed to
76 | # to boot. If a worker process exist with
77 | # this error code, the arbiter will terminate.
78 | _WORKER_BOOT_ERROR = 3
79 |
80 | _WORKERS = {}
81 | _PIPE = []
82 |
83 | # I love dynamic languages
84 | _SIG_QUEUE = []
85 | _SIGNALS = map(
86 | lambda x: getattr(signal, "SIG%s" % x),
87 | "HUP QUIT INT TERM USR1 WINCH".split()
88 | )
89 | _SIG_NAMES = dict(
90 | (getattr(signal, name), name[3:].lower()) for name in dir(signal)
91 | if name[:3] == "SIG" and name[3] != "_"
92 | )
93 |
94 | def __init__(self, args, specs=[], name=None,
95 | child_type="supervisor", age=0, ppid=0,
96 | timeout=30):
97 |
98 | # set conf
99 | conf = DEFAULT_CONF.copy()
100 | conf.update(args)
101 | self.conf = conf
102 |
103 |
104 | specs.extend(self.on_init(conf))
105 |
106 | for spec in specs:
107 | c = Child(*spec)
108 | self._CHILDREN_SPECS.append(c)
109 | self._SPECS_BYNAME[c.name] = c
110 |
111 |
112 | if name is None:
113 | name = self.__class__.__name__
114 | self.name = name
115 | self.child_type = child_type
116 | self.age = age
117 | self.ppid = ppid
118 | self.timeout = timeout
119 |
120 |
121 | self.pid = None
122 | self.num_children = len(self._CHILDREN_SPECS)
123 | self.child_age = 0
124 | self.booted = False
125 | self.stopping = False
126 | self.debug =self.conf.get("debug", False)
127 | self.tmp = WorkerTmp(self.conf)
128 |
129 | def on_init(self, args):
130 | return []
131 |
132 |
133 | def on_init_process(self):
134 | """ method executed when we init a process """
135 | pass
136 |
137 |
138 | def init_process(self):
139 | """\
140 | If you override this method in a subclass, the last statement
141 | in the function should be to call this method with
142 | super(MyWorkerClass, self).init_process() so that the ``run()``
143 | loop is initiated.
144 | """
145 |
146 | # set current pid
147 | self.pid = os.getpid()
148 |
149 | util.set_owner_process(self.conf.get("uid", os.geteuid()),
150 | self.conf.get("gid", os.getegid()))
151 |
152 | # Reseed the random number generator
153 | util.seed()
154 |
155 | # prevent fd inheritance
156 | util.close_on_exec(self.tmp.fileno())
157 |
158 | # init signals
159 | self.init_signals()
160 |
161 | util._setproctitle("arbiter [%s]" % self.name)
162 | self.on_init_process()
163 |
164 | log.debug("Arbiter %s booted on %s", self.name, self.pid)
165 | self.when_ready()
166 | # Enter main run loop
167 | self.booted = True
168 | self.run()
169 |
170 |
171 | def when_ready(self):
172 | pass
173 |
174 | def init_signals(self):
175 | """\
176 | Initialize master signal handling. Most of the signals
177 | are queued. Child signals only wake up the master.
178 | """
179 | if self._PIPE:
180 | map(os.close, self._PIPE)
181 | self._PIPE = pair = os.pipe()
182 | map(util.set_non_blocking, pair)
183 | map(util.close_on_exec, pair)
184 | map(lambda s: signal.signal(s, self.signal), self._SIGNALS)
185 | signal.signal(signal.SIGCHLD, self.handle_chld)
186 |
187 | def signal(self, sig, frame):
188 | if len(self._SIG_QUEUE) < 5:
189 | self._SIG_QUEUE.append(sig)
190 | self.wakeup()
191 | else:
192 | log.warn("Dropping signal: %s", sig)
193 |
194 | def run(self):
195 | "Main master loop."
196 | if not self.booted:
197 | return self.init_process()
198 |
199 | self.spawn_workers()
200 | while True:
201 | try:
202 | # notfy the master
203 | self.tmp.notify()
204 | self.reap_workers()
205 | sig = self._SIG_QUEUE.pop(0) if len(self._SIG_QUEUE) else None
206 | if sig is None:
207 | self.sleep()
208 | self.murder_workers()
209 | self.manage_workers()
210 | continue
211 |
212 | if sig not in self._SIG_NAMES:
213 | log.info("Ignoring unknown signal: %s", sig)
214 | continue
215 |
216 | signame = self._SIG_NAMES.get(sig)
217 | handler = getattr(self, "handle_%s" % signame, None)
218 | if not handler:
219 | log.error("Unhandled signal: %s", signame)
220 | continue
221 | log.info("Handling signal: %s", signame)
222 | handler()
223 | self.tmp.notify()
224 | self.wakeup()
225 | except StopIteration:
226 | self.halt()
227 | except KeyboardInterrupt:
228 | self.halt()
229 | except HaltServer, inst:
230 | self.halt(reason=inst.reason, exit_status=inst.exit_status)
231 | except SystemExit:
232 | raise
233 | except Exception:
234 | log.info("Unhandled exception in main loop:\n%s",
235 | traceback.format_exc())
236 | self.stop(False)
237 | sys.exit(-1)
238 |
239 | def handle_chld(self, sig, frame):
240 | "SIGCHLD handling"
241 | self.wakeup()
242 | self.reap_workers()
243 |
244 | def handle_hup(self):
245 | """\
246 | HUP handling.
247 | - Reload configuration
248 | - Start the new worker processes with a new configuration
249 | - Gracefully shutdown the old worker processes
250 | """
251 | log.info("Hang up: %s", self.name)
252 | self.reload()
253 |
254 | def handle_quit(self):
255 | "SIGQUIT handling"
256 | raise StopIteration
257 |
258 | def handle_int(self):
259 | "SIGINT handling"
260 | raise StopIteration
261 |
262 | def handle_term(self):
263 | "SIGTERM handling"
264 | self.stop(False)
265 | raise StopIteration
266 |
267 | def handle_usr1(self):
268 | """\
269 | SIGUSR1 handling.
270 | Kill all workers by sending them a SIGUSR1
271 | """
272 | self.kill_workers(signal.SIGUSR1)
273 |
274 | def handle_winch(self):
275 | "SIGWINCH handling"
276 | if os.getppid() == 1 or os.getpgrp() != os.getpid():
277 | log.info("graceful stop of workers")
278 | self.num_workers = 0
279 | self.kill_workers(signal.SIGQUIT)
280 | else:
281 | log.info("SIGWINCH ignored. Not daemonized")
282 |
283 | def wakeup(self):
284 | """\
285 | Wake up the arbiter by writing to the _PIPE
286 | """
287 | try:
288 | os.write(self._PIPE[1], '.')
289 | except IOError, e:
290 | if e.errno not in [errno.EAGAIN, errno.EINTR]:
291 | raise
292 |
293 |
294 | def halt(self, reason=None, exit_status=0):
295 | """ halt arbiter """
296 | log.info("Shutting down: %s", self.name)
297 | if reason is not None:
298 | log.info("Reason: %s", reason)
299 | self.stop()
300 | log.info("See you next")
301 | sys.exit(exit_status)
302 |
303 | def sleep(self):
304 | """\
305 | Sleep until _PIPE is readable or we timeout.
306 | A readable _PIPE means a signal occurred.
307 | """
308 | try:
309 | ready = select.select([self._PIPE[0]], [], [], 1.0)
310 | if not ready[0]:
311 | return
312 | while os.read(self._PIPE[0], 1):
313 | pass
314 | except select.error, e:
315 | if e[0] not in [errno.EAGAIN, errno.EINTR]:
316 | raise
317 | except OSError, e:
318 | if e.errno not in [errno.EAGAIN, errno.EINTR]:
319 | raise
320 | except KeyboardInterrupt:
321 | sys.exit()
322 |
323 |
324 | def on_stop(self, graceful=True):
325 | """ method used to pass code when the server start """
326 |
327 | def stop(self, graceful=True):
328 | """\
329 | Stop workers
330 |
331 | :attr graceful: boolean, If True (the default) workers will be
332 | killed gracefully (ie. trying to wait for the current connection)
333 | """
334 |
335 | ## pass any actions before we effectively stop
336 | self.on_stop(graceful=graceful)
337 | self.stopping = True
338 | sig = signal.SIGQUIT
339 | if not graceful:
340 | sig = signal.SIGTERM
341 | limit = time.time() + self.timeout
342 | while True:
343 | if time.time() >= limit or not self._WORKERS:
344 | break
345 | self.kill_workers(sig)
346 | time.sleep(0.1)
347 | self.reap_workers()
348 | self.kill_workers(signal.SIGKILL)
349 | self.stopping = False
350 |
351 | def on_reload(self):
352 | """ method executed on reload """
353 |
354 |
355 | def reload(self):
356 | """
357 | used on HUP
358 | """
359 |
360 | # exec on reload hook
361 | self.on_reload()
362 |
363 | OLD__WORKERS = self._WORKERS.copy()
364 |
365 | # don't kill
366 | to_reload = []
367 |
368 | # spawn new workers with new app & conf
369 | for child in self._CHILDREN_SPECS:
370 | if child.child_type != "supervisor":
371 | to_reload.append(child)
372 |
373 | # set new proc_name
374 | util._setproctitle("arbiter [%s]" % self.name)
375 |
376 | # kill old workers
377 | for wpid, (child, state) in OLD__WORKERS.items():
378 | if state and child.timeout is not None:
379 | if child.child_type == "supervisor":
380 | # we only reload suprvisors.
381 | sig = signal.SIGHUP
382 | elif child.child_type == "brutal_kill":
383 | sig = signal.SIGTERM
384 | else:
385 | sig = signal.SIGQUIT
386 | self.kill_worker(wpid, sig)
387 |
388 |
389 | def murder_workers(self):
390 | """\
391 | Kill unused/idle workers
392 | """
393 | for (pid, child_info) in self._WORKERS.items():
394 | (child, state) = child_info
395 | if state and child.timeout is not None:
396 | try:
397 | diff = time.time() - os.fstat(child.tmp.fileno()).st_ctime
398 | if diff <= child.timeout:
399 | continue
400 | except ValueError:
401 | continue
402 | elif state and child.timeout is None:
403 | continue
404 |
405 | log.critical("WORKER TIMEOUT (pid:%s)", pid)
406 | self.kill_worker(pid, signal.SIGKILL)
407 |
408 | def reap_workers(self):
409 | """\
410 | Reap workers to avoid zombie processes
411 | """
412 | try:
413 | while True:
414 | wpid, status = os.waitpid(-1, os.WNOHANG)
415 | if not wpid:
416 | break
417 |
418 | # A worker said it cannot boot. We'll shutdown
419 | # to avoid infinite start/stop cycles.
420 | exitcode = status >> 8
421 | if exitcode == self._WORKER_BOOT_ERROR:
422 | reason = "Worker failed to boot."
423 | raise HaltServer(reason, self._WORKER_BOOT_ERROR)
424 | child_info = self._WORKERS.pop(wpid, None)
425 |
426 | if not child_info:
427 | continue
428 |
429 | child, state = child_info
430 | child.tmp.close()
431 | if child.child_type in RESTART_WORKERS and not self.stopping:
432 | self._WORKERS["" % id(child)] = (child, 0)
433 | except OSError, e:
434 | if e.errno == errno.ECHILD:
435 | pass
436 |
437 | def manage_workers(self):
438 | """\
439 | Maintain the number of workers by spawning or killing
440 | as required.
441 | """
442 |
443 | for pid, (child, state) in self._WORKERS.items():
444 | if not state:
445 | del self._WORKERS[pid]
446 | self.spawn_child(self._SPECS_BYNAME[child.name])
447 |
448 | def pre_fork(self, worker):
449 | """ methode executed on prefork """
450 |
451 | def post_fork(self, worker):
452 | """ method executed after we forked a worker """
453 |
454 | def spawn_child(self, child_spec):
455 | self.child_age += 1
456 | name = child_spec.name
457 | child_type = child_spec.child_type
458 |
459 | child_args = self.conf
460 | child_args.update(child_spec.args)
461 |
462 | try:
463 | # initialize child class
464 | child = child_spec.child_class(
465 | child_args,
466 | name = name,
467 | child_type = child_type,
468 | age = self.child_age,
469 | ppid = self.pid,
470 | timeout = child_spec.timeout)
471 | except:
472 | log.info("Unhandled exception while creating '%s':\n%s",
473 | name, traceback.format_exc())
474 | return
475 |
476 |
477 | self.pre_fork(child)
478 | pid = os.fork()
479 | if pid != 0:
480 | self._WORKERS[pid] = (child, 1)
481 | return
482 |
483 | # Process Child
484 | worker_pid = os.getpid()
485 | try:
486 | util._setproctitle("worker %s [%s]" % (name, worker_pid))
487 | log.info("Booting %s (%s) with pid: %s", name,
488 | child_type, worker_pid)
489 | self.post_fork(child)
490 | child.init_process()
491 | sys.exit(0)
492 | except SystemExit:
493 | raise
494 | except:
495 | log.exception("Exception in worker process:")
496 | if not child.booted:
497 | sys.exit(self._WORKER_BOOT_ERROR)
498 | sys.exit(-1)
499 | finally:
500 | log.info("Worker exiting (pid: %s)", worker_pid)
501 | try:
502 | child.tmp.close()
503 | except:
504 | pass
505 |
506 | def spawn_workers(self):
507 | """\
508 | Spawn new workers as needed.
509 |
510 | This is where a worker process leaves the main loop
511 | of the master process.
512 | """
513 |
514 | for child in self._CHILDREN_SPECS:
515 | self.spawn_child(child)
516 |
517 | def kill_workers(self, sig):
518 | """\
519 | Lill all workers with the signal `sig`
520 | :attr sig: `signal.SIG*` value
521 | """
522 | for pid in self._WORKERS.keys():
523 | self.kill_worker(pid, sig)
524 |
525 |
526 | def kill_worker(self, pid, sig):
527 | """\
528 | Kill a worker
529 |
530 | :attr pid: int, worker pid
531 | :attr sig: `signal.SIG*` value
532 | """
533 | if not isinstance(pid, int):
534 | return
535 |
536 | try:
537 | os.kill(pid, sig)
538 | except OSError, e:
539 | if e.errno == errno.ESRCH:
540 | try:
541 | (child, info) = self._WORKERS.pop(pid)
542 | child.tmp.close()
543 |
544 | if not self.stopping:
545 | self._WORKERS["" % id(child)] = (child, 0)
546 | return
547 | except (KeyError, OSError):
548 | return
549 | raise
550 |
--------------------------------------------------------------------------------