├── examples ├── __init__.py ├── test.sh ├── file.pdf ├── webclient │ ├── static │ │ ├── circus.gif │ │ └── bread.js │ ├── requirements.txt │ ├── templates │ │ └── index.html │ └── bread.py ├── demo.py ├── example8.ini ├── leaker.py ├── example10.ini ├── hang.ini ├── apis.py ├── simplesocket_client.py ├── verbose_fly.py ├── hang.py ├── byapi.py ├── listener.py ├── flask_app.py ├── max_age.ini ├── example9.ini ├── example6.ini ├── flask_serve.py ├── uwsgi_lossless_reload.ini ├── plugin_watchdog.ini ├── plugin_watchdog.py ├── example7.ini ├── example3.ini ├── flask_redirect.py ├── simplesocket_server.py ├── example1.ini ├── simplesocket.ini ├── example4.ini ├── example2.ini ├── redirect_serve.ini ├── example5.ini ├── addworkers.py ├── dummy_fly2.py └── dummy_fly.py ├── docs ├── source │ ├── _static │ │ └── .empty │ ├── circus-medium.png │ ├── circus-stack.png │ ├── images │ │ ├── circus.png │ │ ├── circus32.png │ │ └── websocket.png │ ├── classical-stack.png │ ├── _themes │ │ └── bootstrap.zip │ ├── for-ops │ │ ├── web-index.png │ │ ├── web-login.png │ │ ├── web-watchers.png │ │ ├── web-add-watcher.png │ │ ├── commands-intro.rst │ │ ├── index.rst │ │ ├── cli.rst │ │ ├── deployment.rst │ │ └── sockets.rst │ ├── design │ │ ├── circus-security.png │ │ ├── circus-architecture.png │ │ ├── index.rst │ │ └── architecture.rst │ ├── tutorial │ │ └── index.rst │ ├── man │ │ ├── index.rst │ │ ├── circus-top.rst │ │ ├── circusd-stats.rst │ │ ├── circus-plugin.rst │ │ ├── circusd.rst │ │ └── circusctl.rst │ ├── _templates │ │ └── indexsidebar.html │ ├── copyright.rst │ ├── glossary.rst │ ├── for-devs │ │ ├── index.rst │ │ └── library.rst │ ├── installation.rst │ └── index.rst └── circus_ext.py ├── tests ├── config │ ├── hooks │ │ ├── __init__.py │ │ └── my_hook.py │ ├── issue651.ini │ ├── include_dir.ini │ ├── issue567.ini │ ├── included-bar.ini │ ├── included-foo.ini │ ├── empty_section.ini │ ├── include.ini │ ├── empty_include.ini │ ├── circus.ini │ ├── hooks.ini │ ├── issue1088.ini │ ├── included │ │ ├── barbaz.ini │ │ └── foobar.ini │ ├── multiple_wildcard │ │ ├── subsubdir1 │ │ │ └── barbaz.ini │ │ └── subsubdir2 │ │ │ └── foobar.ini │ ├── find_hook_in_pythonpath.ini │ ├── issue546.ini │ ├── multiple_wildcard.ini │ ├── issue53.ini │ ├── issue210.ini │ ├── issue137.ini │ ├── test_web.ini │ ├── virtualenv.ini │ ├── issue395.ini │ ├── issue665.ini │ ├── env_var.ini │ ├── issue594.ini │ ├── reload_delplugins.ini │ ├── env_everywhere.ini │ ├── issue310.ini │ ├── reload_delsockets.ini │ ├── issue680.ini │ ├── reuseport.ini │ ├── reload_delwatchers.ini │ ├── env_sensecase.ini │ ├── expand_vars.ini │ ├── reload_base.ini │ ├── reload_changearbiter.ini │ ├── reload_changesockets.ini │ ├── reload_changewatchers.ini │ ├── reload_changeplugins.ini │ ├── issue442.ini │ ├── reload_addwatchers.ini │ ├── reload_numprocesses.ini │ ├── reload_statsd.ini │ ├── reload_addsockets.ini │ ├── reload_addplugins.ini │ ├── env_section.ini │ └── copy_env.ini ├── venv │ └── lib │ │ ├── python3.10 │ │ ├── no-global-site-packages.txt │ │ ├── orig-prefix.txt │ │ └── site-packages │ │ │ └── easy-install.pth │ │ ├── python3.11 │ │ ├── no-global-site-packages.txt │ │ ├── orig-prefix.txt │ │ └── site-packages │ │ │ └── easy-install.pth │ │ ├── python3.12 │ │ ├── no-global-site-packages.txt │ │ ├── orig-prefix.txt │ │ └── site-packages │ │ │ └── easy-install.pth │ │ ├── python3.13 │ │ ├── no-global-site-packages.txt │ │ ├── orig-prefix.txt │ │ └── site-packages │ │ │ └── easy-install.pth │ │ ├── python3.5 │ │ ├── no-global-site-packages.txt │ │ ├── orig-prefix.txt │ │ └── site-packages │ │ │ └── easy-install.pth │ │ ├── python3.6 │ │ ├── no-global-site-packages.txt │ │ ├── orig-prefix.txt │ │ └── site-packages │ │ │ └── easy-install.pth │ │ ├── python3.7 │ │ ├── no-global-site-packages.txt │ │ ├── orig-prefix.txt │ │ └── site-packages │ │ │ └── easy-install.pth │ │ ├── python3.8 │ │ ├── no-global-site-packages.txt │ │ ├── orig-prefix.txt │ │ └── site-packages │ │ │ └── easy-install.pth │ │ └── python3.9 │ │ ├── no-global-site-packages.txt │ │ ├── orig-prefix.txt │ │ └── site-packages │ │ └── easy-install.pth ├── NEED_LOVE.txt ├── test_command_quit.py ├── test_runner.py ├── test_dsa.pub ├── __init__.py ├── test_sighandler.py ├── test_command_list.py ├── test_dsa ├── test_command_decrproc.py ├── generic.py ├── test_plugin_statsd.py ├── test_stats_publisher.py ├── test_convert_option.py ├── test_command_set.py ├── test_command_stats.py ├── test_command_incrproc.py ├── test_pidfile.py ├── test_stdin_socket.py ├── test_plugin_watchdog.py ├── test_plugin_flapping.py ├── test_validate_option.py ├── test_stats_client.py ├── test_controller.py ├── test_stats_streamer.py └── test_plugin_command_reloader.py ├── doc-requirements.txt ├── circus ├── plugins │ ├── _statsd.py │ ├── http_observer.py │ ├── redis_observer.py │ └── command_reloader.py ├── commands │ ├── errors.py │ ├── __init__.py │ ├── reloadconfig.py │ ├── quit.py │ ├── numwatchers.py │ ├── decrproc.py │ ├── listsockets.py │ ├── numprocesses.py │ ├── ipythonshell.py │ ├── listen.py │ ├── status.py │ ├── dstats.py │ ├── stop.py │ ├── list.py │ ├── start.py │ ├── rmwatcher.py │ ├── incrproc.py │ ├── get.py │ ├── globaloptions.py │ ├── set.py │ ├── base.py │ └── options.py ├── fixed_threading.py ├── green │ ├── sighandler.py │ ├── client.py │ ├── consumer.py │ ├── __init__.py │ ├── arbiter.py │ └── controller.py ├── exc.py ├── _patch.py ├── stats │ ├── publisher.py │ └── __init__.py ├── consumer.py ├── pidfile.py └── sighandler.py ├── .coveragerc ├── .gitignore ├── LICENSE ├── README.md ├── Makefile ├── extras └── circusctl_bash_completion ├── .github └── workflows │ ├── validate_release_tag.py │ └── ci.yml └── pyproject.toml /examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/_static/.empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/config/hooks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc-requirements.txt: -------------------------------------------------------------------------------- 1 | mozilla-sphinx-theme 2 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.10/no-global-site-packages.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.11/no-global-site-packages.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.12/no-global-site-packages.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.13/no-global-site-packages.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.5/no-global-site-packages.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.6/no-global-site-packages.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.7/no-global-site-packages.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.8/no-global-site-packages.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.9/no-global-site-packages.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | sleep 1 3 | echo "@@@" 4 | -------------------------------------------------------------------------------- /tests/config/issue651.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 10.5 3 | -------------------------------------------------------------------------------- /tests/config/include_dir.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | include = included-*.ini 3 | -------------------------------------------------------------------------------- /tests/config/issue567.ini: -------------------------------------------------------------------------------- 1 | [watcher:watcher1] 2 | cmd = $(circus.env.GRAVITY) -------------------------------------------------------------------------------- /examples/file.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circus-tent/circus/HEAD/examples/file.pdf -------------------------------------------------------------------------------- /tests/config/included-bar.ini: -------------------------------------------------------------------------------- 1 | [watcher:bar] 2 | cmd = sleep 120 3 | numprocesses = 5 4 | -------------------------------------------------------------------------------- /tests/config/included-foo.ini: -------------------------------------------------------------------------------- 1 | [watcher:foo] 2 | cmd = sleep 120 3 | numprocesses = 5 4 | -------------------------------------------------------------------------------- /tests/config/empty_section.ini: -------------------------------------------------------------------------------- 1 | 2 | [plugin:test_plugin] 3 | 4 | [socket:test_socket] 5 | -------------------------------------------------------------------------------- /tests/config/include.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | include = included-*.ini 3 | include_dir = included 4 | -------------------------------------------------------------------------------- /tests/config/empty_include.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | include = garbage*.ini 3 | include_dir = empty 4 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.10/orig-prefix.txt: -------------------------------------------------------------------------------- 1 | /Library/Frameworks/Python.framework/Versions/3.10 2 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.11/orig-prefix.txt: -------------------------------------------------------------------------------- 1 | /Library/Frameworks/Python.framework/Versions/3.11 2 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.12/orig-prefix.txt: -------------------------------------------------------------------------------- 1 | /Library/Frameworks/Python.framework/Versions/3.12 2 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.13/orig-prefix.txt: -------------------------------------------------------------------------------- 1 | /Library/Frameworks/Python.framework/Versions/3.13 2 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.5/orig-prefix.txt: -------------------------------------------------------------------------------- 1 | /Library/Frameworks/Python.framework/Versions/3.5 2 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.6/orig-prefix.txt: -------------------------------------------------------------------------------- 1 | /Library/Frameworks/Python.framework/Versions/3.5 2 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.7/orig-prefix.txt: -------------------------------------------------------------------------------- 1 | /Library/Frameworks/Python.framework/Versions/3.5 2 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.8/orig-prefix.txt: -------------------------------------------------------------------------------- 1 | /Library/Frameworks/Python.framework/Versions/3.8 2 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.9/orig-prefix.txt: -------------------------------------------------------------------------------- 1 | /Library/Frameworks/Python.framework/Versions/3.9 2 | -------------------------------------------------------------------------------- /tests/config/circus.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | pidfile = pidfile 3 | loglevel = debug 4 | logoutput = logoutput 5 | -------------------------------------------------------------------------------- /tests/config/hooks.ini: -------------------------------------------------------------------------------- 1 | [watcher:foo] 2 | 3 | hooks.before_start = tests.test_config.hook, false 4 | 5 | -------------------------------------------------------------------------------- /docs/source/circus-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circus-tent/circus/HEAD/docs/source/circus-medium.png -------------------------------------------------------------------------------- /docs/source/circus-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circus-tent/circus/HEAD/docs/source/circus-stack.png -------------------------------------------------------------------------------- /docs/source/images/circus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circus-tent/circus/HEAD/docs/source/images/circus.png -------------------------------------------------------------------------------- /docs/source/classical-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circus-tent/circus/HEAD/docs/source/classical-stack.png -------------------------------------------------------------------------------- /docs/source/images/circus32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circus-tent/circus/HEAD/docs/source/images/circus32.png -------------------------------------------------------------------------------- /docs/source/images/websocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circus-tent/circus/HEAD/docs/source/images/websocket.png -------------------------------------------------------------------------------- /docs/source/_themes/bootstrap.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circus-tent/circus/HEAD/docs/source/_themes/bootstrap.zip -------------------------------------------------------------------------------- /docs/source/for-ops/web-index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circus-tent/circus/HEAD/docs/source/for-ops/web-index.png -------------------------------------------------------------------------------- /docs/source/for-ops/web-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circus-tent/circus/HEAD/docs/source/for-ops/web-login.png -------------------------------------------------------------------------------- /tests/NEED_LOVE.txt: -------------------------------------------------------------------------------- 1 | test_green.py (renamed _test_green.py) 2 | 3 | + some FIXME/TODO tests (a few in test_arbiter.py) 4 | -------------------------------------------------------------------------------- /tests/config/issue1088.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | 4 | [watcher:my_app] 5 | cmd = boo 6 | graceful_timeout = 25.5 -------------------------------------------------------------------------------- /circus/plugins/_statsd.py: -------------------------------------------------------------------------------- 1 | # kept for backwards compatibility 2 | from circus.plugins.statsd import StatsdEmitter # NOQA 3 | -------------------------------------------------------------------------------- /docs/source/for-ops/web-watchers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circus-tent/circus/HEAD/docs/source/for-ops/web-watchers.png -------------------------------------------------------------------------------- /examples/webclient/static/circus.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circus-tent/circus/HEAD/examples/webclient/static/circus.gif -------------------------------------------------------------------------------- /docs/source/design/circus-security.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circus-tent/circus/HEAD/docs/source/design/circus-security.png -------------------------------------------------------------------------------- /docs/source/for-ops/web-add-watcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circus-tent/circus/HEAD/docs/source/for-ops/web-add-watcher.png -------------------------------------------------------------------------------- /examples/webclient/requirements.txt: -------------------------------------------------------------------------------- 1 | gevent 2 | flask 3 | hg+https://bitbucket.org/Jeffrey/gevent-websocket#egg=gevent-websocket 4 | -------------------------------------------------------------------------------- /tests/config/hooks/my_hook.py: -------------------------------------------------------------------------------- 1 | 2 | def hook(watcher, arbiter, hook_name, **kwargs): 3 | 'relative_hook' 4 | return True 5 | -------------------------------------------------------------------------------- /tests/config/included/barbaz.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 2 3 | 4 | [watcher:barbaz] 5 | cmd = sleep 120 6 | numprocesses = 5 7 | -------------------------------------------------------------------------------- /docs/source/design/circus-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circus-tent/circus/HEAD/docs/source/design/circus-architecture.png -------------------------------------------------------------------------------- /tests/config/included/foobar.ini: -------------------------------------------------------------------------------- 1 | [watcher:foobar] 2 | cmd = sleep 120 3 | numprocesses = 5 4 | 5 | [env:server] 6 | INI = private.ini 7 | -------------------------------------------------------------------------------- /docs/source/tutorial/index.rst: -------------------------------------------------------------------------------- 1 | Tutorial 2 | ######## 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | step-by-step 8 | rationale 9 | -------------------------------------------------------------------------------- /docs/source/design/index.rst: -------------------------------------------------------------------------------- 1 | Design decisions 2 | ################ 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | architecture 8 | security 9 | -------------------------------------------------------------------------------- /tests/config/multiple_wildcard/subsubdir1/barbaz.ini: -------------------------------------------------------------------------------- 1 | [watcher:barbaz] 2 | cmd = sleep 120 3 | numprocesses = 5 4 | 5 | [env:server] 6 | INI = public.ini 7 | -------------------------------------------------------------------------------- /tests/config/multiple_wildcard/subsubdir2/foobar.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 555 3 | 4 | [watcher:foobar] 5 | cmd = sleep 120 6 | numprocesses = 5 7 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = *_patch*,tests/* 3 | source = circus 4 | include = circus/* 5 | parallel = true 6 | 7 | [html] 8 | directory = html 9 | 10 | -------------------------------------------------------------------------------- /examples/demo.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | def set_var(watcher, arbiter, hook_name): 5 | watcher.env['myvar'] = str(random.randint(10, 100)) 6 | return True 7 | -------------------------------------------------------------------------------- /examples/example8.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | -------------------------------------------------------------------------------- /circus/commands/errors.py: -------------------------------------------------------------------------------- 1 | 2 | NOT_SPECIFIED = 0 3 | INVALID_JSON = 1 4 | UNKNOWN_COMMAND = 2 5 | MESSAGE_ERROR = 3 6 | OS_ERROR = 4 7 | COMMAND_ERROR = 5 8 | BAD_MSG_DATA_ERROR = 6 9 | -------------------------------------------------------------------------------- /tests/config/find_hook_in_pythonpath.ini: -------------------------------------------------------------------------------- 1 | [watcher:foo] 2 | copy_env = True 3 | hooks.before_start = hooks.my_hook.hook 4 | 5 | [env:foo] 6 | PYTHONPATH = $PYTHONPATH:$PWD/tests/config 7 | -------------------------------------------------------------------------------- /tests/config/issue546.ini: -------------------------------------------------------------------------------- 1 | [watcher:test] 2 | cmd = ../bin/chaussette --fd $(circus.sockets.some-socket) 3 | numprocesses = 1 4 | use_sockets = True 5 | 6 | [socket:some-socket] 7 | port = 9090 8 | 9 | -------------------------------------------------------------------------------- /docs/source/man/index.rst: -------------------------------------------------------------------------------- 1 | man pages 2 | ######### 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | circusd 8 | circusctl 9 | circus-plugin 10 | circus-top 11 | circusd-stats 12 | -------------------------------------------------------------------------------- /tests/config/multiple_wildcard.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | include = multiple_wildcard/*/*.ini 3 | 4 | [watcher:server] 5 | cmd = sleep 120 6 | numprocesses = 5 7 | 8 | [env:server] 9 | INI = private.ini 10 | -------------------------------------------------------------------------------- /docs/source/_templates/indexsidebar.html: -------------------------------------------------------------------------------- 1 |
2 |

Feedback

3 | 6 |
7 | -------------------------------------------------------------------------------- /examples/leaker.py: -------------------------------------------------------------------------------- 1 | # sleeps for 55555 then leaks memory 2 | import time 3 | 4 | if __name__ == '__main__': 5 | time.sleep(5) 6 | memory = '' 7 | 8 | while True: 9 | memory += 100000 * ' ' 10 | -------------------------------------------------------------------------------- /circus/fixed_threading.py: -------------------------------------------------------------------------------- 1 | from . import _patch # NOQA 2 | from threading import Thread, RLock, Timer # NOQA 3 | try: 4 | from _thread import get_ident 5 | except ImportError: 6 | from thread import get_ident # NOQA 7 | -------------------------------------------------------------------------------- /examples/example10.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | debug = 1 3 | 4 | [watcher:vars] 5 | cmd = echo $(circus.env.myvar); sleep 1 6 | hooks.before_spawn = examples.demo.set_var 7 | stdout_stream.class = StdoutStream 8 | stderr_stream.class = StdoutStream 9 | -------------------------------------------------------------------------------- /examples/hang.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | 7 | [watcher:dummy] 8 | cmd = ../bin/python hang.py 9 | 10 | -------------------------------------------------------------------------------- /tests/config/issue53.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | 5 | [watcher:test] 6 | cmd = /bin/bash 7 | args = -q # invalid option, makes bash exit non-zero 8 | warmup_delay = 0 9 | numprocesses = 100 10 | -------------------------------------------------------------------------------- /examples/apis.py: -------------------------------------------------------------------------------- 1 | 2 | from circus import get_arbiter 3 | 4 | myprogram = {"cmd": "sleep 30", "numprocesses": 4} 5 | 6 | print('Runnning...') 7 | arbiter = get_arbiter([myprogram]) 8 | try: 9 | arbiter.start() 10 | finally: 11 | arbiter.stop() 12 | -------------------------------------------------------------------------------- /tests/config/issue210.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | statsd = true 7 | 8 | [watcher:my_app] 9 | cmd = boo 10 | graceful_timeout=30 11 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.5/site-packages/easy-install.pth: -------------------------------------------------------------------------------- 1 | import sys; sys.__plen = len(sys.path) 2 | ./pip-7.7-py3.5.egg 3 | import sys; new=sys.path[sys.__plen:]; del sys.path[sys.__plen:]; p=getattr(sys,'__egginsert',0); sys.path[p:p]=new; sys.__egginsert = p+len(new) 4 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.6/site-packages/easy-install.pth: -------------------------------------------------------------------------------- 1 | import sys; sys.__plen = len(sys.path) 2 | ./pip-7.7-py3.5.egg 3 | import sys; new=sys.path[sys.__plen:]; del sys.path[sys.__plen:]; p=getattr(sys,'__egginsert',0); sys.path[p:p]=new; sys.__egginsert = p+len(new) 4 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.7/site-packages/easy-install.pth: -------------------------------------------------------------------------------- 1 | import sys; sys.__plen = len(sys.path) 2 | ./pip-7.7-py3.5.egg 3 | import sys; new=sys.path[sys.__plen:]; del sys.path[sys.__plen:]; p=getattr(sys,'__egginsert',0); sys.path[p:p]=new; sys.__egginsert = p+len(new) 4 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.8/site-packages/easy-install.pth: -------------------------------------------------------------------------------- 1 | import sys; sys.__plen = len(sys.path) 2 | ./pip-7.7-py3.8.egg 3 | import sys; new=sys.path[sys.__plen:]; del sys.path[sys.__plen:]; p=getattr(sys,'__egginsert',0); sys.path[p:p]=new; sys.__egginsert = p+len(new) 4 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.9/site-packages/easy-install.pth: -------------------------------------------------------------------------------- 1 | import sys; sys.__plen = len(sys.path) 2 | ./pip-7.7-py3.9.egg 3 | import sys; new=sys.path[sys.__plen:]; del sys.path[sys.__plen:]; p=getattr(sys,'__egginsert',0); sys.path[p:p]=new; sys.__egginsert = p+len(new) 4 | -------------------------------------------------------------------------------- /tests/config/issue137.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | statsd = true 7 | 8 | [watcher:my_app] 9 | cmd = boo 10 | uid = me 11 | gid = me 12 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.10/site-packages/easy-install.pth: -------------------------------------------------------------------------------- 1 | import sys; sys.__plen = len(sys.path) 2 | ./pip-7.7-py3.10.egg 3 | import sys; new=sys.path[sys.__plen:]; del sys.path[sys.__plen:]; p=getattr(sys,'__egginsert',0); sys.path[p:p]=new; sys.__egginsert = p+len(new) 4 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.11/site-packages/easy-install.pth: -------------------------------------------------------------------------------- 1 | import sys; sys.__plen = len(sys.path) 2 | ./pip-7.7-py3.11.egg 3 | import sys; new=sys.path[sys.__plen:]; del sys.path[sys.__plen:]; p=getattr(sys,'__egginsert',0); sys.path[p:p]=new; sys.__egginsert = p+len(new) 4 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.12/site-packages/easy-install.pth: -------------------------------------------------------------------------------- 1 | import sys; sys.__plen = len(sys.path) 2 | ./pip-7.7-py3.12.egg 3 | import sys; new=sys.path[sys.__plen:]; del sys.path[sys.__plen:]; p=getattr(sys,'__egginsert',0); sys.path[p:p]=new; sys.__egginsert = p+len(new) 4 | -------------------------------------------------------------------------------- /tests/venv/lib/python3.13/site-packages/easy-install.pth: -------------------------------------------------------------------------------- 1 | import sys; sys.__plen = len(sys.path) 2 | ./pip-7.7-py3.13.egg 3 | import sys; new=sys.path[sys.__plen:]; del sys.path[sys.__plen:]; p=getattr(sys,'__egginsert',0); sys.path[p:p]=new; sys.__egginsert = p+len(new) 4 | -------------------------------------------------------------------------------- /tests/config/test_web.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | 7 | [watcher:sleeper] 8 | cmd = sleep 120 9 | warmup_delay = 0 10 | numprocesses = 1 11 | -------------------------------------------------------------------------------- /tests/config/virtualenv.ini: -------------------------------------------------------------------------------- 1 | [watcher:test] 2 | cmd = ../bin/chaussette --fd $(circus.sockets.some-socket) 3 | numprocesses = 1 4 | use_sockets = True 5 | virtualenv = /tmp/.virtualenvs/test 6 | virtualenv_py_ver=3.3 7 | 8 | [socket:some-socket] 9 | port = 9090 10 | 11 | -------------------------------------------------------------------------------- /examples/simplesocket_client.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | # connect to a worker 4 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 5 | sock.connect(('127.0.0.1', 8888)) 6 | 7 | data = sock.recv(100) 8 | print('Received : {0}'.format(repr(data))) 9 | sock.close() 10 | -------------------------------------------------------------------------------- /circus/green/sighandler.py: -------------------------------------------------------------------------------- 1 | import gevent 2 | 3 | from circus.sighandler import SysHandler as _SysHandler 4 | 5 | 6 | class SysHandler(_SysHandler): 7 | 8 | def _register(self): 9 | for sig in self.SIGNALS: 10 | gevent.signal(sig, self.signal, sig) 11 | -------------------------------------------------------------------------------- /examples/verbose_fly.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import time 4 | import sys 5 | 6 | i = 0 7 | 8 | while True: 9 | # print '%d:%d' % (os.getpid(), i) 10 | sys.stdout.write('%d:%d\n' % (os.getpid(), i)) 11 | sys.stdout.flush() 12 | time.sleep(0.1) 13 | i += 1 14 | -------------------------------------------------------------------------------- /tests/config/issue395.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | statsd = true 7 | 8 | [watcher:web] 9 | cmd = foo 10 | args = --fd $(circus.sockets.web) 11 | graceful_timeout = 88 12 | -------------------------------------------------------------------------------- /tests/config/issue665.ini: -------------------------------------------------------------------------------- 1 | [watcher:web1] 2 | cmd = foo 3 | args = --fd 4 | shell = true 5 | 6 | [watcher:web2] 7 | cmd = foo 8 | args = --fd 9 | shell = true 10 | shell_args = bar baz qux 11 | 12 | [watcher:web3] 13 | cmd = foo 14 | args = --fd 15 | shell = false 16 | shell_args = bar baz qux 17 | -------------------------------------------------------------------------------- /tests/config/env_var.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | statsd = True 7 | 8 | [watcher:my_app] 9 | cmd = boo 10 | graceful_timeout = 30 11 | 12 | [env:my_app] 13 | PATH = $PATH:/bin -------------------------------------------------------------------------------- /tests/config/issue594.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | statsd = true 7 | 8 | [watcher:my_app] 9 | cmd = boo 10 | graceful_timeout = 30 11 | stop_signal = INT 12 | stop_children = true -------------------------------------------------------------------------------- /tests/config/reload_delplugins.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = -1 3 | endpoint = tcp://127.0.0.1:7555 4 | pubsub_endpoint = tcp://127.0.0.1:7556 5 | 6 | [watcher:test1] 7 | cmd = sleep 120 8 | 9 | [watcher:test2] 10 | cmd = sleep 120 11 | 12 | [socket:mysocket] 13 | host = localhost 14 | port = 8888 15 | -------------------------------------------------------------------------------- /examples/hang.py: -------------------------------------------------------------------------------- 1 | # import sys 2 | # import StringIO 3 | 4 | from flask import Flask 5 | app = Flask(__name__) 6 | 7 | 8 | @app.route("/") 9 | def hello(): 10 | return "Hello World!" 11 | 12 | if __name__ == "__main__": 13 | # sys.stderr = sys.stdout = StringIO.StringIO() 14 | app.run(port=8000) 15 | -------------------------------------------------------------------------------- /examples/byapi.py: -------------------------------------------------------------------------------- 1 | from circus import get_arbiter 2 | 3 | 4 | myprogram = { 5 | "cmd": "python", 6 | "args": "-u dummy_fly.py $(circus.wid)", 7 | "numprocesses": 3, 8 | } 9 | 10 | 11 | arbiter = get_arbiter([myprogram], debug=True) 12 | try: 13 | arbiter.start() 14 | finally: 15 | arbiter.stop() 16 | -------------------------------------------------------------------------------- /tests/config/env_everywhere.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | endpoint = tcp://127.0.0.1:$(circus.env.ENDPOINT_PORT) 3 | 4 | [socket:bad] 5 | path = /var/run/$(circus.env.BAD_PATH) 6 | 7 | [plugin:breaking] 8 | use = bad.has.been.$(circus.env.WHAT) 9 | 10 | [env] 11 | ENDPOINT_PORT = 1234 12 | BAD_PATH = broken.sock 13 | WHAT = broken -------------------------------------------------------------------------------- /tests/config/issue310.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | statsd = true 7 | 8 | [watcher:web] 9 | cmd = foo 10 | args = --fd $(circus.sockets.web) 11 | 12 | [socket:web] 13 | host = localhost 14 | 15 | -------------------------------------------------------------------------------- /examples/listener.py: -------------------------------------------------------------------------------- 1 | from circus.consumer import CircusConsumer 2 | import json 3 | 4 | 5 | ZMQ_ENDPOINT = 'tcp://127.0.0.1:5556' 6 | topic = 'show:' 7 | 8 | for message, message_topic in CircusConsumer(topic, endpoint=ZMQ_ENDPOINT): 9 | response = json.dumps(dict(message=message, topic=message_topic)) 10 | print(response) 11 | -------------------------------------------------------------------------------- /examples/flask_app.py: -------------------------------------------------------------------------------- 1 | # import resource 2 | 3 | # resource.setrlimit(resource.RLIMIT_NOFILE, (100, 100)) 4 | 5 | from flask import Flask 6 | app = Flask(__name__) 7 | 8 | 9 | @app.route("/") 10 | def hello(): 11 | return "Hello World!" 12 | 13 | 14 | if __name__ == "__main__": 15 | app.run(debug=True, port=8181, host='0.0.0.0') 16 | -------------------------------------------------------------------------------- /tests/config/reload_delsockets.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = -1 3 | endpoint = tcp://127.0.0.1:7555 4 | pubsub_endpoint = tcp://127.0.0.1:7556 5 | 6 | [watcher:test1] 7 | cmd = sleep 120 8 | 9 | [watcher:test2] 10 | cmd = sleep 120 11 | 12 | [plugin:myplugin] 13 | use = circus.plugins.resource_watcher.ResourceWatcher 14 | watcher = test1 15 | -------------------------------------------------------------------------------- /tests/config/issue680.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = -1 3 | 4 | [watcher:test1] 5 | cmd = sleep 10 6 | numprocesses = 2 7 | priority = 10 8 | 9 | [watcher:test2] 10 | cmd = sleep 20 11 | numprocesses = 2 12 | priority = 20 13 | 14 | [plugin:myplugin] 15 | use = circus.plugins.resource_watcher.ResourceWatcher 16 | watcher = test1 17 | priority = 30 18 | -------------------------------------------------------------------------------- /tests/config/reuseport.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | statsd = True 7 | 8 | [socket:reuseport] 9 | host = 127.0.0.1 10 | port = 8888 11 | so_reuseport = True 12 | 13 | [socket:noreuseport] 14 | host = 127.0.0.1 15 | port = 8888 16 | -------------------------------------------------------------------------------- /tests/config/reload_delwatchers.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = -1 3 | endpoint = tcp://127.0.0.1:7555 4 | pubsub_endpoint = tcp://127.0.0.1:7556 5 | 6 | [watcher:test1] 7 | cmd = sleep 120 8 | 9 | [socket:mysocket] 10 | host = localhost 11 | port = 8888 12 | 13 | [plugin:myplugin] 14 | use = circus.plugins.resource_watcher.ResourceWatcher 15 | watcher = test1 16 | -------------------------------------------------------------------------------- /examples/max_age.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | debug = True 6 | 7 | [watcher:dummy] 8 | cmd = python 9 | args = -u dummy_fly2.py $(circus.wid) 10 | warmup_delay = 0 11 | numprocesses = 3 12 | rlimit_nofile = 300 13 | rlimit_nproc = 10 14 | max_age = 10 15 | max_age_variance = 5 16 | -------------------------------------------------------------------------------- /tests/config/env_sensecase.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | statsd = True 7 | 8 | [watcher:webapp] 9 | cmd = curl 10 | 11 | [env:webapp] 12 | http_proxy = http://localhost:8080 13 | HTTPS_PROXY = http://localhost:8043 14 | FunKy_soUl = scorpio 15 | -------------------------------------------------------------------------------- /tests/config/expand_vars.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | 6 | [watcher:echo] 7 | cmd = echo 8 | args = hi 9 | 10 | stdout_stream.class = FileStream 11 | stdout_stream.filename = $(circus.env.LOGDIR)/echo.log 12 | stdout_stream.max_bytes = 10485760 13 | 14 | [env:*] 15 | LOGDIR = /tmp 16 | 17 | -------------------------------------------------------------------------------- /tests/config/reload_base.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = -1 3 | endpoint = tcp://127.0.0.1:7555 4 | pubsub_endpoint = tcp://127.0.0.1:7556 5 | 6 | [watcher:test1] 7 | cmd = sleep 120 8 | 9 | [watcher:test2] 10 | cmd = sleep 120 11 | 12 | [socket:mysocket] 13 | host = localhost 14 | port = 8888 15 | 16 | [plugin:myplugin] 17 | use = circus.plugins.resource_watcher.ResourceWatcher 18 | watcher = test1 19 | -------------------------------------------------------------------------------- /circus/green/client.py: -------------------------------------------------------------------------------- 1 | from circus.client import CircusClient as _CircusClient 2 | 3 | from zmq.green import Context, Poller, POLLIN 4 | 5 | 6 | class CircusClient(_CircusClient): 7 | def _init_context(self, context): 8 | self.context = context or Context.instance() 9 | 10 | def _init_poller(self): 11 | self.poller = Poller() 12 | self.poller.register(self.socket, POLLIN) 13 | -------------------------------------------------------------------------------- /tests/config/reload_changearbiter.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = -2 3 | endpoint = tcp://127.0.0.1:7555 4 | pubsub_endpoint = tcp://127.0.0.1:7556 5 | 6 | [watcher:test1] 7 | cmd = sleep 120 8 | 9 | [watcher:test2] 10 | cmd = sleep 120 11 | 12 | [socket:mysocket] 13 | host = localhost 14 | port = 8888 15 | 16 | [plugin:myplugin] 17 | use = circus.plugins.resource_watcher.ResourceWatcher 18 | watcher = test1 19 | -------------------------------------------------------------------------------- /tests/config/reload_changesockets.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = -1 3 | endpoint = tcp://127.0.0.1:7555 4 | pubsub_endpoint = tcp://127.0.0.1:7556 5 | 6 | [watcher:test1] 7 | cmd = sleep 120 8 | 9 | [watcher:test2] 10 | cmd = sleep 120 11 | 12 | [socket:mysocket] 13 | host = localhost 14 | port = 8889 15 | 16 | [plugin:myplugin] 17 | use = circus.plugins.resource_watcher.ResourceWatcher 18 | watcher = test1 19 | -------------------------------------------------------------------------------- /tests/config/reload_changewatchers.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = -1 3 | endpoint = tcp://127.0.0.1:7555 4 | pubsub_endpoint = tcp://127.0.0.1:7556 5 | 6 | [watcher:test1] 7 | cmd = sleep 120 8 | 9 | [watcher:test2] 10 | cmd = sleep 150 11 | 12 | [socket:mysocket] 13 | host = localhost 14 | port = 8888 15 | 16 | [plugin:myplugin] 17 | use = circus.plugins.resource_watcher.ResourceWatcher 18 | watcher = test1 19 | -------------------------------------------------------------------------------- /circus/green/consumer.py: -------------------------------------------------------------------------------- 1 | from circus.consumer import CircusConsumer as _CircusConsumer 2 | 3 | from zmq.green import Context, Poller, POLLIN 4 | 5 | 6 | class CircusConsumer(_CircusConsumer): 7 | def _init_context(self, context): 8 | self.context = context or Context() 9 | 10 | def _init_poller(self): 11 | self.poller = Poller() 12 | self.poller.register(self.pubsub_socket, POLLIN) 13 | -------------------------------------------------------------------------------- /examples/example9.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | ;debug = 1 7 | 8 | [watcher:leaker] 9 | cmd = python leaker.py 10 | warmup_delay = 0 11 | numprocesses = 1 12 | 13 | [plugin:leak] 14 | use = circus.plugins.resource_watcher.ResourceWatcher 15 | max_mem = 1 16 | max_cpu = 4 17 | service = leaker 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.pyo 4 | circus.egg-info 5 | ^bin 6 | ^lib 7 | ^lib64 8 | ^include 9 | .Python 10 | .coverage 11 | html 12 | docs/source/for-ops/commands/ 13 | docs/source/for-ops/commands.rst 14 | docs/build 15 | local 16 | examples/test.log 17 | man/ 18 | .tox/ 19 | build/ 20 | # ignore patterns for buildout 21 | .installed.cfg 22 | develop-eggs/* 23 | eggs/* 24 | parts/* 25 | *.un~ 26 | *.swp 27 | .pc 28 | .idea 29 | -------------------------------------------------------------------------------- /tests/config/reload_changeplugins.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = -1 3 | endpoint = tcp://127.0.0.1:7555 4 | pubsub_endpoint = tcp://127.0.0.1:7556 5 | 6 | [watcher:test1] 7 | cmd = sleep 120 8 | 9 | [watcher:test2] 10 | cmd = sleep 120 11 | 12 | [socket:mysocket] 13 | host = localhost 14 | port = 8888 15 | 16 | [plugin:myplugin] 17 | use = circus.plugins.resource_watcher.ResourceWatcher 18 | service = test1 19 | watcher = test1 20 | -------------------------------------------------------------------------------- /examples/example6.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | httpd = True 7 | debug = True 8 | httpd_port = 8080 9 | 10 | [watcher:swiss] 11 | cmd = ../bin/python 12 | args = -u flask_app.py 13 | warmup_delay = 0 14 | numprocesses = 1 15 | singleton = True 16 | stdout_stream.class = StdoutStream 17 | stderr_stream.class = StdoutStream 18 | 19 | -------------------------------------------------------------------------------- /tests/config/issue442.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = $(circus.env.circus_stats_endpoint) 6 | statsd = $(circus.env.circus_statsd) 7 | 8 | [watcher:my_app] 9 | cmd = boo 10 | uid = $(circus.env.circus_uid) 11 | gid = $(circus.env.circus_gid) 12 | 13 | 14 | [env] 15 | circus_gid = wheel 16 | circus_uid = tarek 17 | 18 | [env:my_app] 19 | circus_gid = root 20 | -------------------------------------------------------------------------------- /tests/config/reload_addwatchers.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = -1 3 | endpoint = tcp://127.0.0.1:7555 4 | pubsub_endpoint = tcp://127.0.0.1:7556 5 | 6 | [watcher:test1] 7 | cmd = sleep 120 8 | 9 | [watcher:test2] 10 | cmd = sleep 120 11 | 12 | [watcher:test3] 13 | cmd = sleep 120 14 | 15 | [socket:mysocket] 16 | host = localhost 17 | port = 8888 18 | 19 | [plugin:myplugin] 20 | use = circus.plugins.resource_watcher.ResourceWatcher 21 | watcher = test1 22 | -------------------------------------------------------------------------------- /tests/config/reload_numprocesses.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = -1 3 | endpoint = tcp://127.0.0.1:7555 4 | pubsub_endpoint = tcp://127.0.0.1:7556 5 | 6 | [watcher:test1] 7 | cmd = sleep 120 8 | numprocesses = 2 9 | 10 | [watcher:test2] 11 | cmd = sleep 120 12 | numprocesses = 2 13 | 14 | [socket:mysocket] 15 | host = localhost 16 | port = 8888 17 | 18 | [plugin:myplugin] 19 | use = circus.plugins.resource_watcher.ResourceWatcher 20 | watcher = test1 21 | 22 | -------------------------------------------------------------------------------- /examples/flask_serve.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from flask import Flask, make_response 3 | app = Flask(__name__) 4 | app.debug = True 5 | 6 | 7 | @app.route("/") 8 | def pdf(): 9 | pdf = requests.get('http://localhost:5000/file.pdf') 10 | response = make_response(pdf.content) 11 | response.headers['Content-Type'] = "application/pdf" 12 | return response 13 | 14 | 15 | if __name__ == "__main__": 16 | app.run(debug=True, port=8181, host='0.0.0.0') 17 | -------------------------------------------------------------------------------- /tests/config/reload_statsd.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = -1 3 | endpoint = tcp://127.0.0.1:7555 4 | pubsub_endpoint = tcp://127.0.0.1:7556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | statsd = True 7 | 8 | [watcher:test1] 9 | cmd = sleep 120 10 | 11 | [watcher:test2] 12 | cmd = sleep 120 13 | 14 | [socket:mysocket] 15 | host = localhost 16 | port = 8889 17 | 18 | [plugin:myplugin] 19 | use = circus.plugins.resource_watcher.ResourceWatcher 20 | watcher = test1 21 | -------------------------------------------------------------------------------- /examples/uwsgi_lossless_reload.ini: -------------------------------------------------------------------------------- 1 | [watcher:web] 2 | cmd = uwsgi --ini uwsgi.ini --socket fd://$(circus.sockets.web) --stats --stats 127.0.0.1:809$(circus.wid) 3 | stop_signal = QUIT 4 | use_sockets = True 5 | hooks.after_spawn = examples.uwsgi_lossless_reload.children_started 6 | hooks.before_signal = examples.uwsgi_lossless_reload.clean_stop 7 | hooks.extended_stats = examples.uwsgi_lossless_reload.extended_stats 8 | 9 | [socket:web] 10 | host = 127.0.0.1 11 | port = 8888 12 | -------------------------------------------------------------------------------- /tests/config/reload_addsockets.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = -1 3 | endpoint = tcp://127.0.0.1:7555 4 | pubsub_endpoint = tcp://127.0.0.1:7556 5 | 6 | [watcher:test1] 7 | cmd = sleep 120 8 | 9 | [watcher:test2] 10 | cmd = sleep 120 11 | 12 | [socket:mysocket] 13 | host = localhost 14 | port = 8888 15 | 16 | [socket:mysocket2] 17 | host = localhost 18 | port = 8889 19 | 20 | [plugin:myplugin] 21 | use = circus.plugins.resource_watcher.ResourceWatcher 22 | watcher = test1 23 | -------------------------------------------------------------------------------- /examples/plugin_watchdog.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 60 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | 6 | [watcher:test1] 7 | cmd = /usr/bin/python2 examples/plugin_watchdog.py 8 | 9 | [watcher:test2] 10 | cmd = sleep 120 11 | 12 | [watcher:dummy2] 13 | cmd = sleep 120 14 | 15 | [plugin:mywatchdog] 16 | use = circus.plugins.watchdog.WatchDog 17 | loop_rate = 4 18 | watchers_regex = "^test.*$" 19 | msg_regex = "^(?P.*);(?P.*)$" 20 | -------------------------------------------------------------------------------- /examples/plugin_watchdog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import socket 4 | import time 5 | import os 6 | 7 | UDP_IP = "127.0.0.1" 8 | UDP_PORT = 1664 9 | 10 | sock = socket.socket(socket.AF_INET, 11 | socket.SOCK_DGRAM) # UDP 12 | 13 | my_pid = os.getpid() 14 | 15 | for _ in range(25): 16 | message = "{pid};{time}".format(pid=my_pid, time=time.time()) 17 | print('sending:{0}'.format(message)) 18 | sock.sendto(message, (UDP_IP, UDP_PORT)) 19 | time.sleep(2) 20 | -------------------------------------------------------------------------------- /tests/test_command_quit.py: -------------------------------------------------------------------------------- 1 | from tests.test_command_incrproc import FakeArbiter 2 | from tests.support import TestCircus 3 | from circus.commands.quit import Quit 4 | 5 | 6 | class QuitTest(TestCircus): 7 | def test_quit(self): 8 | cmd = Quit() 9 | arbiter = FakeArbiter() 10 | self.assertTrue(arbiter.watchers[0].numprocesses, 1) 11 | props = cmd.message('dummy')['properties'] 12 | cmd.execute(arbiter, props) 13 | self.assertEqual(len(arbiter.watchers), 0) 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/config/reload_addplugins.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = -1 3 | endpoint = tcp://127.0.0.1:7555 4 | pubsub_endpoint = tcp://127.0.0.1:7556 5 | 6 | [watcher:test1] 7 | cmd = sleep 120 8 | 9 | [watcher:test2] 10 | cmd = sleep 120 11 | 12 | [socket:mysocket] 13 | host = localhost 14 | port = 8888 15 | 16 | [plugin:myplugin] 17 | use = circus.plugins.resource_watcher.ResourceWatcher 18 | watcher = test1 19 | 20 | [plugin:myplugin2] 21 | use = circus.plugins.resource_watcher.ResourceWatcher 22 | watcher = test2 23 | 24 | -------------------------------------------------------------------------------- /examples/example7.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | 7 | [watcher:webworker] 8 | cmd = chaussette --fd $(circus.sockets.webapp) flask_app.app 9 | use_sockets = True 10 | copy_path = True 11 | copy_env = True 12 | warmup_delay = 0 13 | numprocesses = 1 14 | stdout_stream.class = StdoutStream 15 | stderr_stream.class = StdoutStream 16 | 17 | [socket:webapp] 18 | path = /tmp/webapp.sock 19 | family = AF_UNIX 20 | -------------------------------------------------------------------------------- /circus/green/__init__.py: -------------------------------------------------------------------------------- 1 | from circus import ArbiterHandler as _ArbiterHandler 2 | 3 | 4 | class ArbiterHandler(_ArbiterHandler): 5 | def __call__(self, watchers, **kwargs): 6 | return super(ArbiterHandler, self).__call__(watchers, **kwargs) 7 | 8 | def _get_arbiter_klass(self, background): 9 | if background: 10 | raise NotImplementedError 11 | else: 12 | from circus.green.arbiter import Arbiter # NOQA 13 | return Arbiter 14 | 15 | 16 | get_arbiter = ArbiterHandler() 17 | -------------------------------------------------------------------------------- /tests/config/env_section.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | statsd = true 6 | 7 | [watcher:watcher1] 8 | cmd = boo 9 | graceful_timeout=30 10 | 11 | [watcher:watcher2] 12 | cmd = boo 13 | graceful_timeout=30 14 | 15 | [env:watcher1] 16 | CAKE = lie 17 | 18 | [env:watcher2] 19 | LIE = cake 20 | 21 | [env:watcher1,watcher2] 22 | PATH = $PATH:/bin 23 | 24 | [env] 25 | TEST1 = test1 26 | 27 | [env:wat*] 28 | TEST2 = test2 29 | 30 | [env:*] 31 | TEST3 = test3 32 | -------------------------------------------------------------------------------- /circus/green/arbiter.py: -------------------------------------------------------------------------------- 1 | from circus.arbiter import Arbiter as _Arbiter 2 | from circus.green.controller import Controller 3 | 4 | from zmq.green.eventloop import ioloop 5 | from zmq.green import Context 6 | 7 | 8 | class Arbiter(_Arbiter): 9 | def _init_context(self, context): 10 | self.context = context or Context.instance() 11 | self.loop = ioloop.IOLoop.current() 12 | self.ctrl = Controller(self.endpoint, self.multicast_endpoint, 13 | self.context, self.loop, self, self.check_delay) 14 | -------------------------------------------------------------------------------- /examples/example3.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | 6 | 7 | [watcher:dying] 8 | cmd = bash test.sh 9 | warmup_delay = 0 10 | numprocesses = 1 11 | flapping.active = False 12 | 13 | 14 | [watcher:dying2] 15 | cmd = bash test.sh 16 | warmup_delay = 0 17 | numprocesses = 1 18 | 19 | flapping.retry_in = 1 20 | flapping.max_retry = 2 21 | flapping.active = True 22 | 23 | [plugin:flapping] 24 | use = circus.plugins.flapping.Flapping 25 | retry_in = 3 26 | max_retry = 2 27 | -------------------------------------------------------------------------------- /tests/test_runner.py: -------------------------------------------------------------------------------- 1 | from tornado.testing import gen_test 2 | from tests.support import TestCircus, async_poll_for 3 | 4 | 5 | def Dummy(test_file): 6 | with open(test_file, 'w') as f: 7 | f.write('..........') 8 | return 1 9 | 10 | 11 | class TestRunner(TestCircus): 12 | 13 | @gen_test 14 | def test_dummy(self): 15 | yield self.start_arbiter('tests.test_runner.Dummy') 16 | res = yield async_poll_for(self.test_file, '..........') 17 | self.assertTrue(res) 18 | yield self.stop_arbiter() 19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/flask_redirect.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, make_response 2 | app = Flask(__name__) 3 | app.debug = True 4 | 5 | 6 | @app.route("/file.pdf") 7 | def file(): 8 | with open('file.pdf', 'rb') as f: 9 | response = make_response(f.read()) 10 | response.headers['Content-Type'] = "application/pdf" 11 | return response 12 | 13 | 14 | @app.route("/") 15 | def page_redirect(): 16 | return redirect("http://localhost:8000") 17 | 18 | 19 | if __name__ == "__main__": 20 | app.run(debug=True, port=8181, host='0.0.0.0') 21 | -------------------------------------------------------------------------------- /tests/config/copy_env.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | statsd = true 6 | 7 | [watcher:watcher1] 8 | cmd = boo 9 | graceful_timeout=30 10 | 11 | [watcher:watcher2] 12 | cmd = boo 13 | copy_env = True 14 | graceful_timeout=30 15 | 16 | [env:watcher1] 17 | CAKE = lie 18 | 19 | [env:watcher2] 20 | LIE = cake 21 | 22 | [env:watcher1,watcher2] 23 | PATH = $PATH:/bin 24 | 25 | [env] 26 | TEST1 = test1 27 | 28 | [env:wat*] 29 | TEST2 = test2 30 | 31 | [env:*] 32 | TEST3 = test3 33 | -------------------------------------------------------------------------------- /circus/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from circus.commands import ( # NOQA 2 | addwatcher, 3 | decrproc, 4 | dstats, 5 | get, 6 | globaloptions, 7 | incrproc, 8 | ipythonshell, 9 | kill, 10 | list, 11 | listen, 12 | listsockets, 13 | numprocesses, 14 | numwatchers, 15 | options, 16 | quit, 17 | reload, 18 | reloadconfig, 19 | restart, 20 | rmwatcher, 21 | sendsignal, 22 | set, 23 | start, 24 | stats, 25 | status, 26 | stop 27 | ) 28 | 29 | from circus.commands.base import get_commands, ok, error # NOQA 30 | -------------------------------------------------------------------------------- /examples/simplesocket_server.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import sys 3 | import time 4 | import os 5 | import random 6 | 7 | fd = int(sys.argv[1]) # getting the FD from circus 8 | sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM) 9 | 10 | # By default socket created by circus is in non-blocking mode. For this example 11 | # we change this. 12 | sock.setblocking(1) 13 | random.seed() 14 | 15 | while True: 16 | conn, addr = sock.accept() 17 | conn.sendall("Hello Circus by %s" % (os.getpid(),)) 18 | seconds = random.randint(2, 12) 19 | time.sleep(seconds) 20 | conn.close() 21 | -------------------------------------------------------------------------------- /circus/exc.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class AlreadyExist(Exception): 4 | """Raised when a watcher exists """ 5 | pass 6 | 7 | 8 | class MessageError(Exception): 9 | """ error raised when a message is invalid """ 10 | pass 11 | 12 | 13 | class CallError(Exception): 14 | pass 15 | 16 | 17 | class ArgumentError(Exception): 18 | """Exception raised when one argument or the number of 19 | arguments is invalid""" 20 | pass 21 | 22 | 23 | class ConflictError(Exception): 24 | """Exception raised when one exclusive command is already running 25 | in background""" 26 | pass 27 | -------------------------------------------------------------------------------- /tests/test_dsa.pub: -------------------------------------------------------------------------------- 1 | ssh-dss AAAAB3NzaC1kc3MAAACBANCrZ1AqHTetbnitHNfUXcTIzx/W0SYDafMg9PgaAncwkmp0azXY84t/0ZaMdxXDdmbeKyaCupnM0t1MKb+/b5t7HxL27kJar2J0OGcS1eYPVnvdm7KPvcIeWNYCxJaVdxLowWnlSVthFVbyEfcajaZfRSEvEGIKxZHBPewbLtB/AAAAFQD2uCiXmypIQ6fPT8KIsnSMbRmWpQAAAIA5f7mLihUjrl5v6pZe9ZVTFI5njPpK+c3Gh01iFUMWGU6UwapWPUQGNmGnOGW5MNFfCNPuOdI6iO/hjSwpUxe1OS2SldCw2V1owOomODDw2k2qzVDIu1RKZqxLV+ikbokTugTlfeFYg3N8ScujjjJJRUih1bwvCxBiBJf8z4QBgAAAAIB55PiceKm6kx4mr/TeYIkVJdUpdnuRLX3KXYCxLDSZHvfndrZYGoJ/xIC5xSgm0TBVe7Lzd8xbYLnQr25xtLxJZIXEd7AdgnqX/HCL63UGAAf2mzbNLTmwwF2fXOLQKNbuM7wF2fnb/PHU9TtgZ48Y65M9X4Q/qEUi5mVBlni9gg== nick@nick-VirtualBox 2 | -------------------------------------------------------------------------------- /examples/example1.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | httpd = True 7 | debug = True 8 | 9 | [watcher:dummy] 10 | cmd = python 11 | args = -u dummy_fly.py $(circus.wid) 12 | warmup_delay = 0 13 | numprocesses = 5 14 | 15 | [watcher:dummy2] 16 | cmd = python 17 | args = -u dummy_fly2.py $(circus.wid) 18 | warmup_delay = 0 19 | numprocesses = 3 20 | rlimit_nofile = 300 21 | rlimit_nproc = 10 22 | 23 | [plugin:flapping] 24 | use = circus.plugins.flapping.Flapping 25 | retry_in = 3 26 | max_retry = 2 27 | -------------------------------------------------------------------------------- /examples/simplesocket.ini: -------------------------------------------------------------------------------- 1 | ; this is an example for a simple socket process. 2 | ; simplesocket_server.py is automatically launched by circus. 3 | ; You have to then run simplesocket_client.py multiple times yourself 4 | ; (in a terminal) 5 | 6 | [circus] 7 | check_delay = 5 8 | endpoint = tcp://127.0.0.1:5555 9 | pubsub_endpoint = tcp://127.0.0.1:5556 10 | stats_endpoint = tcp://127.0.0.1:5557 11 | 12 | [watcher:worker] 13 | cmd = python simplesocket_server.py $(circus.sockets.simplesock) 14 | use_sockets = True 15 | warmup_delay = 0 16 | numprocesses = 3 17 | 18 | [socket:simplesock] 19 | host = 127.0.0.1 20 | port = 8888 21 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import zmq 4 | from circus.util import configure_logger 5 | from circus import logger 6 | 7 | 8 | _CONFIGURED = False 9 | 10 | if not _CONFIGURED and 'TESTING' in os.environ: 11 | configure_logger(logger, level='CRITICAL', output=os.devnull) 12 | _CONFIGURED = True 13 | 14 | 15 | def tearDown(): 16 | # There seems to some issue with context cleanup and Python >= 3.4 17 | # making the tests hang at the end 18 | # Explicitly destroying the context seems to do the trick 19 | # cf https://github.com/zeromq/pyzmq/pull/513 20 | zmq.Context.instance().destroy() 21 | -------------------------------------------------------------------------------- /examples/example4.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | 7 | [watcher:dummy] 8 | cmd = python 9 | args = -u dummy_fly.py $WID 10 | warmup_delay = 0 11 | numprocesses = 5 12 | 13 | [watcher:dummy2] 14 | cmd = python 15 | args = -u dummy_fly2.py $WID 16 | warmup_delay = 0 17 | numprocesses = 3 18 | rlimit_nofile = 300 19 | rlimit_nproc = 10 20 | 21 | [plugin:statsd] 22 | use = circus.plugins._statsd.StatsdEmitter 23 | 24 | host = localhost 25 | port = 8125 26 | sample_rate = 1.0 27 | application_name = example4 28 | -------------------------------------------------------------------------------- /tests/test_sighandler.py: -------------------------------------------------------------------------------- 1 | from tornado.testing import gen_test 2 | 3 | from tests.support import TestCircus, async_poll_for 4 | 5 | 6 | class TestSigHandler(TestCircus): 7 | 8 | @gen_test 9 | def test_handler(self): 10 | yield self.start_arbiter() 11 | 12 | # wait for the process to be started 13 | res = yield async_poll_for(self.test_file, 'START') 14 | self.assertTrue(res) 15 | 16 | # stopping... 17 | yield self.arbiter.stop() 18 | 19 | # wait for the process to be stopped 20 | res = yield async_poll_for(self.test_file, 'QUIT') 21 | self.assertTrue(res) 22 | 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012 - Mozilla Foundation 2 | Copyright 2012 - Benoit Chesneau 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | -------------------------------------------------------------------------------- /tests/test_command_list.py: -------------------------------------------------------------------------------- 1 | from tests.support import TestCircus 2 | from circus.commands.list import List 3 | 4 | 5 | class ListCommandTest(TestCircus): 6 | 7 | def test_list_watchers(self): 8 | cmd = List() 9 | self.assertTrue( 10 | cmd.console_msg({'watchers': ['foo', 'bar']}), 11 | 'foo,bar') 12 | 13 | def test_list_processors(self): 14 | cmd = List() 15 | self.assertTrue( 16 | cmd.console_msg({'pids': [12, 13]}), '12,13') 17 | 18 | def test_list_error(self): 19 | cmd = List() 20 | self.assertTrue("error" in cmd.console_msg({'foo': 'bar'})) 21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build Status](https://github.com/circus-tent/circus/workflows/ci/badge.svg) 2 | ![Coverage Status](https://coveralls.io/repos/github/circus-tent/circus/badge.svg?branch=master) 3 | ![PyPI](https://img.shields.io/pypi/v/circus) 4 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/circus) 5 | # Circus 6 | 7 | Circus is a program that runs and watches processes and sockets. 8 | Circus can be used as a library or through the command line. 9 | 10 | ## Links 11 | 12 | - [Full Documentation](https://circus.readthedocs.io) 13 | - [How to Contribute](https://circus.readthedocs.io/en/latest/contributing/) 14 | - IRC: Freenode, channel #mozilla-circus 15 | -------------------------------------------------------------------------------- /docs/source/for-ops/commands-intro.rst: -------------------------------------------------------------------------------- 1 | .. _commands: 2 | 3 | Commands 4 | ######## 5 | 6 | At the epicenter of circus lives the command systems. *circusctl* is just a 7 | zeromq client, and if needed you can drive programmaticaly the Circus system by 8 | writing your own zmq client. 9 | 10 | All messages are JSON mappings. 11 | 12 | For each command below, we provide a usage example with circusctl but also the 13 | input / output zmq messages. 14 | 15 | .. The actual list of commands is generated by the docs/circus_ext.py file. 16 | It will append the list of commands to the content above. Documentation 17 | contributors can safely edit the text above this comment when making 18 | improvements. 19 | -------------------------------------------------------------------------------- /examples/example2.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | ;stream_backend = gevent 7 | 8 | [watcher:verbose] 9 | cmd = python 10 | args = -u verbose_fly.py 11 | warmup_delay = 0 12 | numprocesses = 10 13 | 14 | ; stream options 15 | stdout_stream.class = FileStream 16 | stdout_stream.filename = test.log 17 | stdout_stream.refresh_time = 0.3 18 | stdout_stream.time_format = '%Y/%m/%d | %H:%M:%S' 19 | 20 | [watcher:verbose2] 21 | cmd = python 22 | args = -u verbose_fly.py 23 | numprocesses = 4 24 | 25 | [plugin:flapping] 26 | use = circus.plugins.flapping.Flapping 27 | retry_in = 3 28 | max_retry = 2 29 | -------------------------------------------------------------------------------- /tests/test_dsa: -------------------------------------------------------------------------------- 1 | -----BEGIN DSA PRIVATE KEY----- 2 | MIIBugIBAAKBgQDQq2dQKh03rW54rRzX1F3EyM8f1tEmA2nzIPT4GgJ3MJJqdGs1 3 | 2POLf9GWjHcVw3Zm3ismgrqZzNLdTCm/v2+bex8S9u5CWq9idDhnEtXmD1Z73Zuy 4 | j73CHljWAsSWlXcS6MFp5UlbYRVW8hH3Go2mX0UhLxBiCsWRwT3sGy7QfwIVAPa4 5 | KJebKkhDp89PwoiydIxtGZalAoGAOX+5i4oVI65eb+qWXvWVUxSOZ4z6SvnNxodN 6 | YhVDFhlOlMGqVj1EBjZhpzhluTDRXwjT7jnSOojv4Y0sKVMXtTktkpXQsNldaMDq 7 | Jjgw8NpNqs1QyLtUSmasS1fopG6JE7oE5X3hWINzfEnLo44ySUVIodW8LwsQYgSX 8 | /M+EAYACgYB55PiceKm6kx4mr/TeYIkVJdUpdnuRLX3KXYCxLDSZHvfndrZYGoJ/ 9 | xIC5xSgm0TBVe7Lzd8xbYLnQr25xtLxJZIXEd7AdgnqX/HCL63UGAAf2mzbNLTmw 10 | wF2fXOLQKNbuM7wF2fnb/PHU9TtgZ48Y65M9X4Q/qEUi5mVBlni9ggIUSpjsGRl9 11 | acODwvnb4zHc/BNK4PY= 12 | -----END DSA PRIVATE KEY----- 13 | -------------------------------------------------------------------------------- /examples/redirect_serve.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | httpd = True 7 | 8 | [watcher:serve] 9 | cmd = ../bin/chaussette --fd $(circus.sockets.serve) flask_serve.app 10 | use_sockets = True 11 | numprocesses = 2 12 | stdout_stream.class = StdoutStream 13 | stderr_stream.class = StdoutStream 14 | 15 | [socket:serve] 16 | host = 0.0.0.0 17 | port = 8000 18 | 19 | [watcher:redirect] 20 | cmd = ../bin/chaussette --fd $(circus.sockets.redirect) flask_redirect.app 21 | use_sockets = True 22 | numprocesses = 3 23 | stdout_stream.class = StdoutStream 24 | stderr_stream.class = StdoutStream 25 | 26 | [socket:redirect] 27 | host = 0.0.0.0 28 | port = 5000 29 | -------------------------------------------------------------------------------- /circus/green/controller.py: -------------------------------------------------------------------------------- 1 | from circus.util import AsyncPeriodicCallback 2 | from circus.controller import Controller as _Controller 3 | from circus.green.sighandler import SysHandler 4 | 5 | from zmq.green.eventloop import zmqstream 6 | 7 | 8 | class Controller(_Controller): 9 | 10 | def _init_syshandler(self): 11 | self.sys_hdl = SysHandler(self) 12 | 13 | def _init_stream(self): 14 | self.stream = zmqstream.ZMQStream(self.ctrl_socket, self.loop) 15 | self.stream.on_recv(self.handle_message) 16 | 17 | def start(self): 18 | self.loop.make_current() 19 | self.initialize() 20 | self.caller = AsyncPeriodicCallback(self.arbiter.manage_watchers, 21 | self.check_delay) 22 | self.caller.start() 23 | -------------------------------------------------------------------------------- /examples/example5.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | httpd = 1 7 | httpd_host = 127.0.0.1 8 | httpd_port = 8080 9 | debug = 1 10 | 11 | [watcher:webworker] 12 | ; chaussette is an external project. If you want to test this file, you'll need 13 | ; to install it and point this example to where it's been installed. 14 | cmd = ../bin/chaussette --fd $(circus.sockets.webapp) --pre-hook chaussette.util.setup_bench --post-hook chaussette.util.teardown_bench chaussette.util.bench_app 15 | use_sockets = True 16 | warmup_delay = 0 17 | numprocesses = 3 18 | stdout_stream.class = StdoutStream 19 | stderr_stream.class = StdoutStream 20 | 21 | [socket:webapp] 22 | host = 127.0.0.1 23 | port = 8888 24 | -------------------------------------------------------------------------------- /examples/addworkers.py: -------------------------------------------------------------------------------- 1 | from circus.client import CircusClient 2 | from circus.util import DEFAULT_ENDPOINT_DEALER 3 | 4 | client = CircusClient(endpoint=DEFAULT_ENDPOINT_DEALER) 5 | 6 | command = '../bin/python dummy_fly.py 111' 7 | name = 'dummy' 8 | 9 | 10 | for i in range(50): 11 | print(client.call(""" 12 | { 13 | "command": "add", 14 | "properties": { 15 | "cmd": "%s", 16 | "name": "%s", 17 | "options": { 18 | "copy_env": true, 19 | "stdout_stream": { 20 | "filename": "stdout.log" 21 | }, 22 | "stderr_stream": { 23 | "filename": "stderr.log" 24 | } 25 | }, 26 | "start": true 27 | } 28 | } 29 | """ % (command, name + str(i)))) 30 | -------------------------------------------------------------------------------- /examples/dummy_fly2.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | import sys 4 | import time 5 | 6 | 7 | class DummyFly(object): 8 | 9 | def __init__(self, wid): 10 | self.wid = wid 11 | # init signal handling 12 | signal.signal(signal.SIGQUIT, self.handle_quit) 13 | signal.signal(signal.SIGTERM, self.handle_quit) 14 | signal.signal(signal.SIGINT, self.handle_quit) 15 | signal.signal(signal.SIGCHLD, self.handle_chld) 16 | self.alive = True 17 | 18 | def handle_quit(self, *args): 19 | self.alive = False 20 | sys.exit(0) 21 | 22 | def handle_chld(self, *args): 23 | return 24 | 25 | def run(self): 26 | print("hello, fly 2 #%s (pid: %s) is alive" % (self.wid, os.getpid())) 27 | 28 | while self.alive: 29 | time.sleep(0.1) 30 | 31 | if __name__ == "__main__": 32 | DummyFly(sys.argv[1]).run() 33 | -------------------------------------------------------------------------------- /docs/source/man/circus-top.rst: -------------------------------------------------------------------------------- 1 | circus-top man page 2 | ################### 3 | 4 | Synopsis 5 | -------- 6 | 7 | circus-top [options] 8 | 9 | 10 | Description 11 | ----------- 12 | 13 | circus-top is a *top*-like command to display the Circus daemon and 14 | processes managed by circus. 15 | 16 | 17 | Options 18 | ------- 19 | 20 | :--endpoint *ENDPOINT*: 21 | Connection endpoint. 22 | 23 | :--ssh *SSH*: 24 | SSH Server in the format ``user@host:port``. 25 | 26 | :--process-timeout *PROCESS_TIMEOUT*: 27 | After this delay of inactivity, a process will be removed. 28 | 29 | :-h, \--help: 30 | Show the help message and exit. 31 | 32 | :\--version: 33 | Displays Circus version and exits. 34 | 35 | 36 | See also 37 | -------- 38 | 39 | `circus` (1), `circusctl` (1), `circusd` (1), `circusd-stats` (1), `circus-plugin` (1). 40 | 41 | Full Documentation is available at https://circus.readthedocs.io 42 | -------------------------------------------------------------------------------- /examples/dummy_fly.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import signal 4 | import sys 5 | 6 | 7 | class DummyFly(object): 8 | 9 | def __init__(self, wid): 10 | self.wid = wid 11 | # init signal handling 12 | signal.signal(signal.SIGQUIT, self.handle_quit) 13 | signal.signal(signal.SIGTERM, self.handle_quit) 14 | signal.signal(signal.SIGINT, self.handle_quit) 15 | signal.signal(signal.SIGCHLD, self.handle_chld) 16 | self.alive = True 17 | 18 | def handle_quit(self, *args): 19 | self.alive = False 20 | sys.exit(0) 21 | 22 | def handle_chld(self, *args): 23 | return 24 | 25 | def run(self): 26 | print("hello, fly #%s (pid: %s) is alive" % (self.wid, os.getpid())) 27 | 28 | a = 2 29 | while self.alive: 30 | a = a + 200 31 | 32 | if __name__ == "__main__": 33 | DummyFly(sys.argv[1]).run() 34 | -------------------------------------------------------------------------------- /tests/test_command_decrproc.py: -------------------------------------------------------------------------------- 1 | import tests.test_command_incrproc as tci 2 | from tests.support import TestCircus 3 | from circus.commands.decrproc import DecrProc 4 | 5 | 6 | class DecrProcTest(TestCircus): 7 | 8 | def test_decr_proc(self): 9 | cmd = DecrProc() 10 | arbiter = tci.FakeArbiter() 11 | self.assertTrue(arbiter.watchers[0].numprocesses, 1) 12 | 13 | props = cmd.message('dummy')['properties'] 14 | cmd.execute(arbiter, props) 15 | self.assertEqual(arbiter.watchers[0].numprocesses, 0) 16 | 17 | def test_decr_proc_singleton(self): 18 | cmd = DecrProc() 19 | arbiter = tci.FakeArbiterWithSingletonWatchers() 20 | size_before = arbiter.watchers[0].numprocesses 21 | 22 | props = cmd.message('dummy', 3)['properties'] 23 | cmd.execute(arbiter, props) 24 | self.assertEqual(arbiter.watchers[0].numprocesses, size_before) 25 | 26 | 27 | -------------------------------------------------------------------------------- /circus/_patch.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from threading import _active_limbo_lock, _active, _sys 3 | 4 | 5 | debugger = False 6 | try: 7 | # noinspection PyUnresolvedReferences 8 | import pydevd 9 | debugger = pydevd.GetGlobalDebugger() 10 | except ImportError: 11 | pass 12 | 13 | if not debugger: 14 | # see http://bugs.python.org/issue1596321 15 | if hasattr(threading.Thread, '_Thread__delete'): 16 | def _delete(self): 17 | try: 18 | with _active_limbo_lock: 19 | del _active[self._Thread__ident] 20 | except KeyError: 21 | if 'dummy_threading' not in _sys.modules: 22 | raise 23 | 24 | threading.Thread._Thread__delete = _delete 25 | else: 26 | def _delete(self): # NOQA 27 | try: 28 | with _active_limbo_lock: 29 | del _active[self._ident] 30 | except KeyError: 31 | if 'dummy_threading' not in _sys.modules: 32 | raise 33 | 34 | threading.Thread._delete = _delete 35 | -------------------------------------------------------------------------------- /docs/source/copyright.rst: -------------------------------------------------------------------------------- 1 | Copyright 2 | ######### 3 | 4 | Circus was initiated by Tarek Ziade and is licenced under APLv2 5 | 6 | Benoit Chesneau was an early contributor and did many things, like most of 7 | the circus.commands work. 8 | 9 | 10 | Licence 11 | ======= 12 | 13 | :: 14 | 15 | Copyright 2012 - Mozilla Foundation 16 | Copyright 2012 - Benoit Chesneau 17 | 18 | Licensed under the Apache License, Version 2.0 (the "License"); 19 | you may not use this file except in compliance with the License. 20 | You may obtain a copy of the License at 21 | 22 | http://www.apache.org/licenses/LICENSE-2.0 23 | 24 | Unless required by applicable law or agreed to in writing, software 25 | distributed under the License is distributed on an "AS IS" BASIS, 26 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 27 | See the License for the specific language governing permissions and 28 | limitations under the License. 29 | 30 | Contributors 31 | ============ 32 | 33 | See the full list at https://github.com/circus-tent/circus/blob/master/CONTRIBUTORS.txt 34 | 35 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: docs build test coverage build_rpm clean 2 | 3 | ifndef VTENV_OPTS 4 | VTENV_OPTS = -p python2.7 --no-site-packages 5 | endif 6 | 7 | VENV?=virtualenv 8 | 9 | bin/python: 10 | $(VENV) $(VTENV_OPTS) . 11 | bin/python setup.py develop 12 | 13 | test: bin/python 14 | bin/pip install tox 15 | bin/tox 16 | 17 | docs: 18 | bin/pip install -r doc-requirements.txt --use-mirrors 19 | SPHINXBUILD=../bin/sphinx-build $(MAKE) -C docs html $^ 20 | 21 | coverage: bin/coverage 22 | rm -f `pwd`/.coverage 23 | rm -rf `pwd`/html 24 | - COVERAGE_PROCESS_START=`pwd`/.coveragerc COVERAGE_FILE=`pwd`/.coverage PYTHONPATH=`pwd` bin/pytest -s tests 25 | bin/coverage combine 26 | bin/coverage html 27 | 28 | bin/coverage: bin/python 29 | bin/pip install -r test-requirements.txt --use-mirrors 30 | bin/pip install pytest pytest-cov 31 | 32 | build_rpm: 33 | bin/python setup.py bdist_rpm --requires "python26 python-setuptools pyzmq python26-psutil" 34 | 35 | clean: 36 | rm -rf bin .tox include/ lib/ man/ circus.egg-info/ build/ 37 | find . -name "*.pyc" | xargs rm -f 38 | find . -name "*.un~" | xargs rm -f 39 | find . -name "__pycache__" | xargs rm -rf 40 | -------------------------------------------------------------------------------- /circus/commands/reloadconfig.py: -------------------------------------------------------------------------------- 1 | from circus.commands.base import Command 2 | 3 | 4 | class ReloadConfig(Command): 5 | """\ 6 | Reload the configuration file 7 | ============================= 8 | 9 | This command reloads the configuration file, so changes in the 10 | configuration file will be reflected in the configuration of 11 | circus. 12 | 13 | 14 | ZMQ Message 15 | ----------- 16 | 17 | :: 18 | 19 | { 20 | "command": "reloadconfig", 21 | "waiting": False 22 | } 23 | 24 | The response return the status "ok". If the property graceful is 25 | set to true the processes will be exited gracefully. 26 | 27 | 28 | Command line 29 | ------------ 30 | 31 | :: 32 | 33 | $ circusctl reloadconfig [--waiting] 34 | 35 | """ 36 | name = "reloadconfig" 37 | options = Command.waiting_options 38 | 39 | def message(self, *args, **opts): 40 | return self.make_message(**opts) 41 | 42 | def execute(self, arbiter, props): 43 | return arbiter.reload_from_config() 44 | -------------------------------------------------------------------------------- /circus/stats/publisher.py: -------------------------------------------------------------------------------- 1 | import zmq 2 | import zmq.utils.jsonapi as json 3 | 4 | from circus import logger 5 | from circus.util import to_bytes 6 | 7 | 8 | class StatsPublisher(object): 9 | def __init__(self, stats_endpoint='tcp://127.0.0.1:5557', context=None): 10 | self.ctx = context or zmq.Context() 11 | self.destroy_context = context is None 12 | self.stats_endpoint = stats_endpoint 13 | self.socket = self.ctx.socket(zmq.PUB) 14 | self.socket.bind(self.stats_endpoint) 15 | self.socket.linger = 0 16 | 17 | def publish(self, name, stat): 18 | try: 19 | topic = 'stat.%s' % str(name) 20 | if 'subtopic' in stat: 21 | topic += '.%d' % stat['subtopic'] 22 | 23 | stat = json.dumps(stat) 24 | logger.debug('Sending %s' % stat) 25 | self.socket.send_multipart([to_bytes(topic), stat]) 26 | 27 | except zmq.ZMQError: 28 | if self.socket.closed: 29 | pass 30 | else: 31 | raise 32 | 33 | def stop(self): 34 | if self.destroy_context: 35 | self.ctx.destroy(0) 36 | logger.debug('Publisher stopped') 37 | -------------------------------------------------------------------------------- /circus/commands/quit.py: -------------------------------------------------------------------------------- 1 | from circus.commands.base import Command 2 | 3 | 4 | class Quit(Command): 5 | """\ 6 | Quit the arbiter immediately 7 | ============================ 8 | 9 | When the arbiter receive this command, the arbiter exit. 10 | 11 | ZMQ Message 12 | ----------- 13 | 14 | :: 15 | 16 | { 17 | "command": "quit", 18 | "waiting": False 19 | } 20 | 21 | The response return the status "ok". 22 | 23 | If ``waiting`` is False (default), the call will return immediately 24 | after calling ``stop_signal`` on each process. 25 | 26 | If ``waiting`` is True, the call will return only when the stop process 27 | is completely ended. Because of the 28 | :ref:`graceful_timeout option `, it can take some 29 | time. 30 | 31 | 32 | Command line 33 | ------------ 34 | 35 | :: 36 | 37 | $ circusctl quit [--waiting] 38 | 39 | """ 40 | name = "quit" 41 | options = Command.waiting_options 42 | 43 | def message(self, *args, **opts): 44 | return self.make_message(**opts) 45 | 46 | def execute(self, arbiter, props): 47 | return arbiter.stop() 48 | -------------------------------------------------------------------------------- /circus/commands/numwatchers.py: -------------------------------------------------------------------------------- 1 | from circus.commands.base import Command 2 | from circus.exc import ArgumentError 3 | 4 | 5 | class NumWatchers(Command): 6 | """\ 7 | Get the number of watchers 8 | ========================== 9 | 10 | Get the number of watchers in a arbiter 11 | 12 | ZMQ Message 13 | ----------- 14 | 15 | :: 16 | 17 | { 18 | "command": "numwatchers", 19 | } 20 | 21 | The response return the number of watchers in the 'numwatchers` 22 | property:: 23 | 24 | { "status": "ok", "numwatchers": , "time", "timestamp" } 25 | 26 | 27 | Command line 28 | ------------ 29 | 30 | :: 31 | 32 | $ circusctl numwatchers 33 | 34 | """ 35 | name = "numwatchers" 36 | 37 | def message(self, *args, **opts): 38 | if len(args) > 0: 39 | raise ArgumentError("Invalid number of arguments") 40 | return self.make_message() 41 | 42 | def execute(self, arbiter, props): 43 | return {"numwatchers": arbiter.numwatchers()} 44 | 45 | def console_msg(self, msg): 46 | if msg.get("status") == "ok": 47 | return str(msg.get("numwatchers")) 48 | return self.console_error(msg) 49 | -------------------------------------------------------------------------------- /docs/source/man/circusd-stats.rst: -------------------------------------------------------------------------------- 1 | circusd-stats man page 2 | ###################### 3 | 4 | Synopsis 5 | -------- 6 | 7 | circusd-stats [options] 8 | 9 | 10 | Description 11 | ----------- 12 | 13 | circusd-stats runs the stats aggregator for Circus. 14 | 15 | 16 | Options 17 | ------- 18 | 19 | :--endpoint *ENDPOINT*: 20 | Connection endpoint. 21 | 22 | :--pubsub *PUBSUB*: 23 | The circusd ZeroMQ pub/sub socket to connect to. 24 | 25 | :--statspoint *STATSPOINT*: 26 | The ZeroMQ pub/sub socket to send data to. 27 | 28 | :\--log-level *LEVEL*: 29 | Specify the log level. *LEVEL* can be `info`, `debug`, `critical`, 30 | `warning` or `error`. 31 | 32 | :\--log-output *LOGOUTPUT*: 33 | The location where the logs will be written. The default behavior is to 34 | write to stdout (you can force it by passing '-' to this option). Takes 35 | a filename otherwise. 36 | 37 | :--ssh *SSH*: 38 | SSH Server in the format ``user@host:port``. 39 | 40 | :-h, \--help: 41 | Show the help message and exit. 42 | 43 | :\--version: 44 | Displays Circus version and exits. 45 | 46 | 47 | See also 48 | -------- 49 | 50 | `circus` (1), `circusd` (1), `circusctl` (1), `circus-plugin` (1), `circus-top` (1). 51 | 52 | Full Documentation is available at https://circus.readthedocs.io 53 | -------------------------------------------------------------------------------- /extras/circusctl_bash_completion: -------------------------------------------------------------------------------- 1 | # ######################################################################### 2 | # This bash script adds tab-completion feature to circusctl 3 | # 4 | # Testing it out without installing 5 | # ================================= 6 | # 7 | # To test out the completion without "installing" this, just run this file 8 | # directly, like so: 9 | # 10 | # source ~/path/to/circusctl_bash_completion 11 | # 12 | # After you do that, tab completion will immediately be made available in your 13 | # current Bash shell. But it won't be available next time you log in. 14 | # 15 | # Installing 16 | # ========== 17 | # 18 | # To install this, point to this file from your .bash_profile, like so: 19 | # 20 | # source ~/path/to/circusctl_bash_completion 21 | # 22 | # Do the same in your .bashrc if .bashrc doesn't invoke .bash_profile. 23 | # 24 | # Settings will take effect the next time you log in. 25 | # 26 | # Uninstalling 27 | # ============ 28 | # 29 | # To uninstall, just remove the line from your .bash_profile and .bashrc. 30 | 31 | _circusctl_completion() 32 | { 33 | COMPREPLY=( $( COMP_WORDS="${COMP_WORDS[*]}" \ 34 | COMP_CWORD=$COMP_CWORD \ 35 | AUTO_COMPLETE=1 $1 ) ) 36 | } 37 | complete -F _circusctl_completion -o default circusctl 38 | -------------------------------------------------------------------------------- /docs/source/glossary.rst: -------------------------------------------------------------------------------- 1 | .. _glossary: 2 | 3 | Glossary: Circus-specific terms 4 | ############################### 5 | 6 | .. glossary:: 7 | :sorted: 8 | 9 | watcher 10 | watchers 11 | A *watcher* is the program you tell Circus to run. A single Circus 12 | instance can run one or more watchers. 13 | 14 | worker 15 | workers 16 | process 17 | processes 18 | A *process* is an independent OS process instance of your program. 19 | A single watcher can run one or more processes. We also call them 20 | workers. 21 | 22 | arbiter 23 | The *arbiter* is responsible for managing all the watchers within 24 | circus, ensuring all processes run correctly. 25 | 26 | controller 27 | A *controller* contains the set of actions that can be performed on 28 | the arbiter. 29 | 30 | pub/sub 31 | Circus has a *pubsub* that receives events from the watchers and 32 | dispatches them to all subscribers. 33 | 34 | flapping 35 | The *flapping detection* subscribes to events and detects when some 36 | processes are constantly restarting. 37 | 38 | remote controller 39 | The *remote controller* allows you to communicate with the controller 40 | via ZMQ to control Circus. 41 | -------------------------------------------------------------------------------- /examples/webclient/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Circus Web Client 6 | 7 | 16 | 17 | 18 | 19 | 20 |

Circus

21 | 22 | 23 |
24 | 25 | 26 |
27 | 28 | 29 |

30 | 
31 |     
38 |     
39 |   
40 | 
41 | 


--------------------------------------------------------------------------------
/tests/generic.py:
--------------------------------------------------------------------------------
 1 | import sys
 2 | 
 3 | sys.path.insert(0, './')
 4 | 
 5 | 
 6 | def resolve_name(name):
 7 |     ret = None
 8 |     parts = name.split('.')
 9 |     cursor = len(parts)
10 |     module_name = parts[:cursor]
11 |     last_exc = None
12 | 
13 |     while cursor > 0:
14 |         try:
15 |             ret = __import__('.'.join(module_name))
16 |             break
17 |         except ImportError as exc:
18 |             last_exc = exc
19 |             if cursor == 0:
20 |                 raise
21 |             cursor -= 1
22 |             module_name = parts[:cursor]
23 | 
24 |     for part in parts[1:]:
25 |         try:
26 |             ret = getattr(ret, part)
27 |         except AttributeError:
28 |             if last_exc is not None:
29 |                 raise last_exc
30 |             raise ImportError(name)
31 | 
32 |     if ret is None:
33 |         if last_exc is not None:
34 |             raise last_exc
35 |         raise ImportError(name)
36 | 
37 |     return ret
38 | 
39 | 
40 | if __name__ == '__main__':
41 |     callback = resolve_name(sys.argv[1])
42 |     try:
43 |         if len(sys.argv) > 2:
44 |             test_file = sys.argv[2]
45 |             sys.exit(callback(test_file))
46 |         else:
47 |             sys.exit(callback())
48 |     except:  # noqa: E722
49 |         sys.exit(1)
50 | 


--------------------------------------------------------------------------------
/tests/test_plugin_statsd.py:
--------------------------------------------------------------------------------
 1 | from tornado.testing import gen_test
 2 | 
 3 | from tests.support import TestCircus, async_poll_for
 4 | from tests.support import async_run_plugin
 5 | from circus.plugins.statsd import FullStats
 6 | 
 7 | 
 8 | def get_gauges(queue, plugin):
 9 |     queue.put(plugin.statsd.gauges)
10 | 
11 | 
12 | class TestFullStats(TestCircus):
13 | 
14 |     @gen_test
15 |     def test_full_stats(self):
16 |         dummy_process = 'tests.support.run_process'
17 |         yield self.start_arbiter(dummy_process)
18 |         yield async_poll_for(self.test_file, 'START')
19 | 
20 |         config = {'loop_rate': 0.2}
21 |         gauges = yield async_run_plugin(
22 |             FullStats, config,
23 |             plugin_info_callback=get_gauges,
24 |             duration=1000,
25 |             endpoint=self.arbiter.endpoint,
26 |             pubsub_endpoint=self.arbiter.pubsub_endpoint)
27 | 
28 |         # we should have a bunch of stats events here
29 |         self.assertTrue(len(gauges) >= 5)
30 |         last_batch = sorted(name for name, value in gauges[-5:])
31 |         wanted = ['_stats.test.cpu_sum', '_stats.test.mem_max',
32 |                   '_stats.test.mem_pct_max', '_stats.test.mem_pct_sum',
33 |                   '_stats.test.mem_sum']
34 |         self.assertEqual(last_batch, wanted)
35 | 
36 |         yield self.stop_arbiter()
37 | 
38 | 
39 | 


--------------------------------------------------------------------------------
/docs/circus_ext.py:
--------------------------------------------------------------------------------
 1 | import os
 2 | from circus.commands import get_commands
 3 | 
 4 | 
 5 | def generate_commands(app):
 6 |     path = os.path.join(app.srcdir, "for-ops", "commands")
 7 |     ext = app.config['source_suffix']
 8 |     if not os.path.exists(path):
 9 |         os.makedirs(path)
10 | 
11 |     tocname = os.path.join(app.srcdir, "for-ops", "commands%s" % ext)
12 | 
13 |     commands = get_commands()
14 |     items = commands.items()
15 |     items = sorted(items)
16 | 
17 |     with open(tocname, "w") as toc:
18 |         toc.write(".. include:: commands-intro%s\n\n" % ext)
19 |         toc.write("circus-ctl commands\n")
20 |         toc.write("-------------------\n\n")
21 | 
22 |         commands = get_commands()
23 |         for name, cmd in items:
24 |             toc.write("- **%s**: :doc:`commands/%s`\n" % (name, name))
25 | 
26 |             # write the command file
27 |             refline = ".. _%s:" % name
28 |             fname = os.path.join(path, "%s%s" % (name, ext))
29 |             with open(fname, "w") as f:
30 |                 f.write("\n".join([refline, "\n", cmd.desc, ""]))
31 | 
32 |         toc.write("\n")
33 |         toc.write(".. toctree::\n")
34 |         toc.write("   :hidden:\n")
35 |         toc.write("   :glob:\n\n")
36 |         toc.write("   commands/*\n")
37 | 
38 | 
39 | def setup(app):
40 |     app.connect('builder-inited', generate_commands)
41 | 


--------------------------------------------------------------------------------
/examples/webclient/static/bread.js:
--------------------------------------------------------------------------------
 1 | // JavaScript Code for Bread: A Sample Circus Client
 2 | 
 3 | 
 4 | // TODO: Be friendly to browsers that don't have WebSockets, or use socket.io
 5 | (function (globals) {
 6 |     var bread = {};
 7 | 
 8 |     var host = globals.location.hostname,
 9 |         port = 5000;
10 | 
11 |     var el = document.getElementsByTagName('pre')[0];
12 | 
13 |     function getSocketUrl(topic) {
14 |         return ['ws://', host, ':', port, '/api?topic=', topic].join('');
15 |     }
16 | 
17 |     function log(msg) {
18 |         var args = Array.prototype.slice.call(arguments, 0),
19 |             code = document.createElement('code');
20 |         code.textContent = msg;
21 |         el.appendChild(code);
22 |     }
23 | 
24 |     bread.subscribe = function (topic) {
25 |         var endpoint = getSocketUrl(topic),
26 |             socket = new globals.WebSocket(endpoint);
27 | 
28 |         socket.onopen = function (e) {
29 |             console.log('open');
30 |             log('Listening at ' + endpoint + '...');
31 |         };
32 | 
33 |         socket.onmessage = function (e) {
34 |             console.log('message');
35 |             log(e.data);
36 |         };
37 | 
38 |         socket.onclose = function (e) {
39 |             console.log('closed');
40 |             log('Socket closed.');
41 |         };
42 |     };
43 | 
44 |     globals.bread = bread;
45 | 
46 | }(this));
47 | 


--------------------------------------------------------------------------------
/docs/source/man/circus-plugin.rst:
--------------------------------------------------------------------------------
 1 | circus-plugin man page
 2 | ######################
 3 | 
 4 | Synopsis
 5 | --------
 6 | 
 7 | circus-plugin [options] [plugin]
 8 | 
 9 | 
10 | Description
11 | -----------
12 | 
13 | circus-plugin allows to launch a plugin from a running Circus daemon.
14 | 
15 | 
16 | Arguments
17 | ---------
18 | 
19 | :plugin: Fully qualified name of the plugin class.
20 | 	 
21 | 
22 | Options
23 | -------
24 | 
25 | :--endpoint *ENDPOINT*:
26 |    Connection endpoint.
27 | 
28 | :--pubsub *PUBSUB*:
29 |    The circusd ZeroMQ pub/sub socket to connect to.
30 | 
31 | :--config *CONFIG*: The plugin configuration file.
32 | 
33 | :--check-delay *CHECK_DELAY*: Check delay.
34 | 		    
35 | :\--log-level *LEVEL*:
36 |    Specify the log level. *LEVEL* can be `info`, `debug`, `critical`,
37 |    `warning` or `error`.
38 | 
39 | :\--log-output *LOGOUTPUT*:
40 |    The location where the logs will be written. The default behavior is to
41 |    write to stdout (you can force it by passing '-' to this option). Takes
42 |    a filename otherwise.
43 | 
44 | :--ssh *SSH*:
45 |    SSH Server in the format ``user@host:port``.
46 | 
47 | :-h, \--help:
48 |    Show the help message and exit.
49 | 
50 | :\--version:
51 |    Displays Circus version and exits.
52 | 
53 | 
54 | See also
55 | --------
56 | 
57 | `circus` (1), `circusd` (1), `circusctl` (1), `circusd-stats` (1), `circus-top` (1).
58 | 
59 | Full Documentation is available at http://circus.readthedocs.org
60 | 


--------------------------------------------------------------------------------
/tests/test_stats_publisher.py:
--------------------------------------------------------------------------------
 1 | from unittest import mock
 2 | 
 3 | import zmq
 4 | import zmq.utils.jsonapi as json
 5 | 
 6 | from tests.support import TestCase
 7 | from circus.stats.publisher import StatsPublisher
 8 | 
 9 | 
10 | class TestStatsPublisher(TestCase):
11 |     def setUp(self):
12 |         self.publisher = StatsPublisher()
13 |         self.origin_socket = self.publisher.socket
14 |         self.publisher.socket = mock.MagicMock()
15 | 
16 |     def tearDown(self):
17 |         self.publisher.socket = self.origin_socket
18 |         self.publisher.stop()
19 |         del self.origin_socket
20 | 
21 |     def test_publish(self):
22 |         stat = {'subtopic': 1, 'foo': 'bar'}
23 |         self.publisher.publish('foobar', stat)
24 |         self.publisher.socket.send_multipart.assert_called_with(
25 |             [b'stat.foobar.1', json.dumps(stat)])
26 | 
27 |     def test_publish_reraise_zmq_errors(self):
28 |         self.publisher.socket.closed = False
29 |         self.publisher.socket.send_multipart.side_effect = zmq.ZMQError()
30 | 
31 |         stat = {'subtopic': 1, 'foo': 'bar'}
32 |         self.assertRaises(zmq.ZMQError, self.publisher.publish, 'foobar', stat)
33 | 
34 |     def test_publish_silent_zmq_errors_when_socket_closed(self):
35 |         self.publisher.socket.closed = True
36 |         self.publisher.socket.send_multipart.side_effect = zmq.ZMQError()
37 | 
38 |         stat = {'subtopic': 1, 'foo': 'bar'}
39 |         self.publisher.publish('foobar', stat)
40 | 
41 | 
42 | 


--------------------------------------------------------------------------------
/circus/plugins/http_observer.py:
--------------------------------------------------------------------------------
 1 | 
 2 | from circus.plugins.statsd import BaseObserver
 3 | try:
 4 |     from tornado.httpclient import AsyncHTTPClient
 5 | except ImportError:
 6 |     raise ImportError("This plugin requires tornado-framework to run.")
 7 | 
 8 | 
 9 | class HttpObserver(BaseObserver):
10 | 
11 |     name = 'http_observer'
12 |     default_app_name = "http_observer"
13 | 
14 |     def __init__(self, *args, **config):
15 |         super(HttpObserver, self).__init__(*args, **config)
16 |         self.http_client = AsyncHTTPClient(io_loop=self.loop)
17 |         self.check_url = config.get("check_url", "http://localhost/")
18 |         self.timeout = float(config.get("timeout", 10))
19 | 
20 |         self.restart_on_error = config.get("restart_on_error", None)
21 | 
22 |     def look_after(self):
23 | 
24 |         def handle_response(response, *args, **kwargs):
25 |             if response.error:
26 |                 self.statsd.increment("http_stats.error")
27 |                 self.statsd.increment("http_stats.error.%s" % response.code)
28 |                 if self.restart_on_error:
29 |                     self.cast("restart", name=self.restart_on_error)
30 |                     self.statsd.increment("http_stats.restart_on_error")
31 |                 return
32 | 
33 |             self.statsd.timed("http_stats.request_time",
34 |                               int(response.request_time * 1000))
35 | 
36 |         self.http_client.fetch(self.check_url, handle_response,
37 |                                request_timeout=self.timeout)
38 | 


--------------------------------------------------------------------------------
/docs/source/man/circusd.rst:
--------------------------------------------------------------------------------
 1 | circusd man page
 2 | ################
 3 | 
 4 | Synopsis
 5 | --------
 6 | 
 7 | circusd [options] [config]
 8 | 
 9 | 
10 | Description
11 | -----------
12 | 
13 | circusd is the main process of the Circus architecture. It takes care of
14 | running all the processes. Each process managed by Circus is a child
15 | process of **circusd**.
16 | 
17 | 
18 | Arguments
19 | ---------
20 | 
21 | :config: configuration file
22 | 
23 | 
24 | Options
25 | -------
26 | 
27 | :-h, \--help:
28 |    Show the help message and exit
29 | 
30 | :\--log-level *LEVEL*:
31 |    Specify the log level. *LEVEL* can be `info`, `debug`, `critical`,
32 |    `warning` or `error`.
33 | 
34 | :\--log-output *LOGOUTPUT*:
35 |    The location where the logs will be written. The default behavior is to
36 |    write to stdout (you can force it by passing '-' to this option). Takes
37 |    a filename otherwise.
38 | 
39 | :\--logger-config *LOGGERCONFIG*:
40 |    The location where a standard Python logger configuration INI, JSON or YAML
41 |    file can be found. This can be used to override the default logging
42 |    configuration for the arbiter.
43 | 
44 | :\--daemon:
45 |    Start circusd in the background.
46 | 
47 | :\--pidfile *PIDFILE*:
48 |    The location of the PID file.
49 | 
50 | :\--version:
51 |    Displays Circus version and exits.
52 | 
53 | 
54 | See also
55 | --------
56 | 
57 | `circus` (1), `circusctl` (1), `circusd-stats` (1), `circus-plugin` (1), `circus-top` (1).
58 | 
59 | Full Documentation is available at https://circus.readthedocs.io
60 | 


--------------------------------------------------------------------------------
/docs/source/for-ops/index.rst:
--------------------------------------------------------------------------------
 1 | .. _forops:
 2 | 
 3 | Circus for Ops
 4 | ##############
 5 | 
 6 | .. warning::
 7 | 
 8 |    By default, Circus doesn't secure its messages when sending information
 9 |    through ZeroMQ. Before running Circus in a production environment, make sure
10 |    to read the :ref:`Security` page.
11 | 
12 | The first step to manage a Circus daemon is to write its configuration file.
13 | See :ref:`configuration`. If you are deploying a web stack, have a look at
14 | :ref:`sockets`.
15 | 
16 | Circus can be deployed using Python 2.7, 3.2 or 3.3 - most deployments
17 | out there are done in 2.7. To learn how to deploy Circus, check out
18 | :ref:`deployment`.
19 | 
20 | To manage a Circus daemon, you should get familiar with the list of
21 | :ref:`commands` you can use in **circusctl**. Notice that you can have the same
22 | help online when you run **circusctl** as a shell.
23 | 
24 | We also provide **circus-top**, see :ref:`cli`, and a nice web dashboard, see
25 | :ref:`circushttpd`.
26 | 
27 | For quick watcher and process management – start, stop, increment, decrement
28 | etc – there is a Tcl/Tk interface. See `Ringmaster `_.
29 | 
30 | Last, to get the most out of Circus, make sure to check out how
31 | to use plugins and hooks. See :ref:`plugins` and :ref:`hooks`.
32 | 
33 | 
34 | Ops documentation index
35 | -----------------------
36 | 
37 | .. toctree::
38 |    :maxdepth: 1
39 | 
40 |    configuration
41 |    commands
42 |    cli
43 |    circusweb
44 |    sockets
45 |    using-plugins
46 |    deployment
47 | 


--------------------------------------------------------------------------------
/examples/webclient/bread.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | """
 3 |     Bread: A Simple Web Client for Circus
 4 | 
 5 | """
 6 | from gevent.pywsgi import WSGIServer
 7 | from geventwebsocket.handler import WebSocketHandler
 8 | 
 9 | import json
10 | import argparse
11 | 
12 | from circus.consumer import CircusConsumer
13 | from flask import Flask, request, render_template
14 | 
15 | 
16 | ZMQ_ENDPOINT = 'tcp://127.0.0.1:5556'
17 | 
18 | app = Flask(__name__)
19 | 
20 | 
21 | @app.route('/')
22 | def index():
23 |     return render_template('index.html')
24 | 
25 | 
26 | @app.route('/api')
27 | def api():
28 |     """WebSocket endpoint; Takes a 'topic' GET param."""
29 |     ws = request.environ.get('wsgi.websocket')
30 |     topic = request.args.get('topic')
31 | 
32 |     if None in (ws, topic):
33 |         return
34 | 
35 |     topic = topic.encode('ascii')
36 |     for message, message_topic in CircusConsumer(topic, endpoint=ZMQ_ENDPOINT):
37 |         response = json.dumps(dict(message=message, topic=message_topic))
38 |         ws.send(response)
39 | 
40 | 
41 | def main():
42 |     parser = argparse.ArgumentParser(description=__doc__)
43 |     parser.add_argument('--host', default='127.0.0.1')
44 |     parser.add_argument('--port', default=5000)
45 |     args = parser.parse_args()
46 |     server_loc = (args.host, args.port)
47 |     print('HTTP Server running at http://%s:%s/...' % server_loc)
48 |     http_server = WSGIServer(server_loc, app, handler_class=WebSocketHandler)
49 |     http_server.serve_forever()
50 | 
51 | 
52 | if __name__ == '__main__':
53 |     main()
54 | 


--------------------------------------------------------------------------------
/docs/source/for-ops/cli.rst:
--------------------------------------------------------------------------------
 1 | .. _cli:
 2 | 
 3 | CLI tools
 4 | #########
 5 | 
 6 | circus-top
 7 | ==========
 8 | 
 9 | *circus-top* is a top-like console you can run to watch
10 | live your running Circus system. It will display the CPU, Memory
11 | usage and socket hits if you have some.
12 | 
13 | 
14 | Example of output::
15 | 
16 |     -----------------------------------------------------------------------
17 |     circusd-stats
18 |      PID                 CPU (%)             MEMORY (%)
19 |     14252                 0.8                 0.4
20 |                           0.8 (avg)           0.4 (sum)
21 | 
22 |     dummy
23 |      PID                 CPU (%)             MEMORY (%)
24 |     14257                 78.6                0.1
25 |     14256                 76.6                0.1
26 |     14258                 74.3                0.1
27 |     14260                 71.4                0.1
28 |     14259                 70.7                0.1
29 |                           74.32 (avg)         0.5 (sum)
30 | 
31 |     ----------------------------------------------------------------------
32 | 
33 | 
34 | 
35 | *circus-top* is a read-only console. If you want to interact with the system, use
36 | *circusctl*.
37 | 
38 | 
39 | circusctl
40 | =========
41 | 
42 | *circusctl* can be used to run any command listed in :ref:`commands` . For
43 | example, you can get a list of all the watchers, you can do ::
44 | 
45 |     $ circusctl list
46 | 
47 | Besides supporting a handful of options you can also specify the endpoint
48 | *circusctl* should use using the ``CIRCUSCTL_ENDPOINT`` environment variable.
49 | 


--------------------------------------------------------------------------------
/.github/workflows/validate_release_tag.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | """Validate that the version in the tag label matches the version of the package."""
 3 | import argparse
 4 | import ast
 5 | from pathlib import Path
 6 | 
 7 | 
 8 | def get_version_from_module(content: str) -> str:
 9 |     """Get the ``__version__`` attribute from a module.
10 | 
11 |     .. note:: This has been adapted from :mod:`setuptools.config`.
12 |     """
13 |     try:
14 |         module = ast.parse(content)
15 |     except SyntaxError as exception:
16 |         raise IOError('Unable to parse module.') from exception
17 | 
18 |     try:
19 |         return next(
20 |             ast.literal_eval(statement.value) for statement in module.body if isinstance(statement, ast.Assign)
21 |             for target in statement.targets if isinstance(target, ast.Name) and target.id == '__version__'
22 |         )
23 |     except StopIteration as exception:
24 |         raise IOError('Unable to find the `__version__` attribute in the module.') from exception
25 | 
26 | 
27 | if __name__ == '__main__':
28 |     parser = argparse.ArgumentParser()
29 |     parser.add_argument('GITHUB_REF', help='The GITHUB_REF environmental variable')
30 |     args = parser.parse_args()
31 |     tag_prefix = 'refs/tags/'
32 |     assert args.GITHUB_REF.startswith(tag_prefix), f'GITHUB_REF should start with "{tag_prefix}": {args.GITHUB_REF}'
33 |     tag_version = args.GITHUB_REF.removeprefix(tag_prefix)
34 |     package_version = get_version_from_module(Path('circus/__init__.py').read_text(encoding='utf-8'))
35 |     error_message = f'The tag version `{tag_version}` is different from the package version `{package_version}`'
36 |     assert tag_version == package_version, error_message
37 | 


--------------------------------------------------------------------------------
/docs/source/for-ops/deployment.rst:
--------------------------------------------------------------------------------
 1 | .. _deployment:
 2 | 
 3 | Deployment
 4 | ##########
 5 | 
 6 | Although the Circus daemon can be managed with the circusd command, it's
 7 | easier to have it start on boot. If your system supports Upstart, you can
 8 | create this Upstart script in /etc/init/circus.conf.
 9 | 
10 | ::
11 | 
12 |     start on filesystem and net-device-up IFACE=lo
13 |     stop on runlevel [016]
14 | 
15 |     respawn
16 |     exec /usr/local/bin/circusd /etc/circus/circusd.ini
17 | 
18 | This assumes that circusd.ini is located at /etc/circus/circusd.ini. After
19 | rebooting, you can control circusd with the service command::
20 | 
21 |     # service circus start/stop/restart
22 | 
23 | If your system supports systemd, you can create this systemd unit file under
24 | /etc/systemd/system/circus.service.
25 | 
26 | ::
27 | 
28 |    [Unit]
29 |    Description=Circus process manager
30 |    After=syslog.target network.target nss-lookup.target
31 | 
32 |    [Service]
33 |    Type=simple
34 |    ExecReload=/usr/bin/circusctl reload
35 |    ExecStart=/usr/bin/circusd /etc/circus/circus.ini
36 |    Restart=always
37 |    RestartSec=5
38 | 
39 |    [Install]
40 |    WantedBy=default.target
41 | 
42 | A reboot isn't required if you run the daemon-reload command below::
43 | 
44 |     # systemctl --system daemon-reload
45 | 
46 | Then circus can be managed via::
47 | 
48 |     # systemctl start/stop/status/reload circus
49 | 
50 | 
51 | Recipes
52 | =======
53 | 
54 | This section will contain recipes to deploy Circus. Until then you can look at
55 | Pete's `Puppet recipe `_ or at Remy's
56 | `Chef recipe
57 | `_
58 | 


--------------------------------------------------------------------------------
/circus/commands/decrproc.py:
--------------------------------------------------------------------------------
 1 | from circus.commands.incrproc import IncrProc
 2 | from circus.util import TransformableFuture
 3 | 
 4 | 
 5 | class DecrProc(IncrProc):
 6 |     """\
 7 |         Decrement the number of processes in a watcher
 8 |         ==============================================
 9 | 
10 |         This comment decrement the number of processes in a watcher
11 |         by , 1 being the default.
12 | 
13 |         ZMQ Message
14 |         -----------
15 | 
16 |         ::
17 | 
18 |             {
19 |                 "command": "decr",
20 |                 "propeties": {
21 |                     "name": ""
22 |                     "nb": 
23 |                     "waiting": False
24 |                 }
25 |             }
26 | 
27 |         The response return the number of processes in the 'numprocesses`
28 |         property::
29 | 
30 |             { "status": "ok", "numprocesses": , "time", "timestamp" }
31 | 
32 |         Command line
33 |         ------------
34 | 
35 |         ::
36 | 
37 |             $ circusctl decr  [] [--waiting]
38 | 
39 |         Options
40 |         +++++++
41 | 
42 |         - : name of the watcher
43 |         - : the number of processes to remove.
44 | 
45 |     """
46 |     name = "decr"
47 |     properties = ['name']
48 | 
49 |     def execute(self, arbiter, props):
50 |         watcher = self._get_watcher(arbiter, props.get('name'))
51 |         if watcher.singleton:
52 |             return {"numprocesses": watcher.numprocesses, "singleton": True}
53 |         else:
54 |             nb = props.get('nb', 1)
55 |             resp = TransformableFuture()
56 |             resp.set_upstream_future(watcher.decr(nb))
57 |             resp.set_transform_function(lambda x: {"numprocesses": x})
58 |             return resp
59 | 


--------------------------------------------------------------------------------
/circus/plugins/redis_observer.py:
--------------------------------------------------------------------------------
 1 | 
 2 | from circus.plugins.statsd import BaseObserver
 3 | 
 4 | try:
 5 |     import redis
 6 | except ImportError:
 7 |     raise ImportError("This plugin requires the redis-lib to run.")
 8 | 
 9 | 
10 | class RedisObserver(BaseObserver):
11 | 
12 |     name = 'redis_observer'
13 |     default_app_name = "redis_observer"
14 | 
15 |     OBSERVE = ['pubsub_channels', 'connected_slaves', 'lru_clock',
16 |                'connected_clients', 'keyspace_misses', 'used_memory',
17 |                'used_memory_peak', 'total_commands_processed',
18 |                'used_memory_rss', 'total_connections_received',
19 |                'pubsub_patterns', 'used_cpu_sys', 'used_cpu_sys_children',
20 |                'blocked_clients', 'used_cpu_user', 'client_biggest_input_buf',
21 |                'mem_fragmentation_ratio', 'expired_keys', 'evicted_keys',
22 |                'client_longest_output_list', 'uptime_in_seconds',
23 |                'keyspace_hits']
24 | 
25 |     def __init__(self, *args, **config):
26 |         super(RedisObserver, self).__init__(*args, **config)
27 |         self.redis = redis.from_url(config.get("redis_url",
28 |                                     "redis://localhost:6379/0"),
29 |                                     float(config.get("timeout", 5)))
30 | 
31 |         self.restart_on_timeout = config.get("restart_on_timeout", None)
32 | 
33 |     def look_after(self):
34 |         try:
35 |             info = self.redis.info()
36 |         except redis.ConnectionError:
37 |             self.statsd.increment("redis_stats.error")
38 |             if self.restart_on_timeout:
39 |                 self.cast("restart", name=self.restart_on_timeout)
40 |                 self.statsd.increment("redis_stats.restart_on_error")
41 |             return
42 | 
43 |         for key in self.OBSERVE:
44 |             self.statsd.gauge("redis_stats.%s" % key, info[key])
45 | 


--------------------------------------------------------------------------------
/docs/source/for-devs/index.rst:
--------------------------------------------------------------------------------
 1 | .. _fordevs:
 2 | 
 3 | Circus for developers
 4 | #####################
 5 | 
 6 | 
 7 | Using Circus as a library
 8 | -------------------------
 9 | 
10 | Circus provides high-level classes and functions that will let you manage
11 | processes in your own applications.
12 | 
13 | For example, if you want to run four processes forever, you could write:
14 | 
15 | .. code-block:: python
16 | 
17 |     from circus import get_arbiter
18 | 
19 |     myprogram = {"cmd": "python myprogram.py", "numprocesses": 4}
20 | 
21 |     arbiter = get_arbiter([myprogram])
22 |     try:
23 |         arbiter.start()
24 |     finally:
25 |         arbiter.stop()
26 | 
27 | This snippet will run four instances of *myprogram* and watch them for you,
28 | restarting them if they die unexpectedly.
29 | 
30 | To learn more about this, see :ref:`library`
31 | 
32 | 
33 | Extending Circus
34 | ----------------
35 | 
36 | It's easy to extend Circus to create a more complex system, by listening to all
37 | the **circusd** events via its pub/sub channel, and driving it via commands.
38 | 
39 | That's how the flapping feature works for instance: it listens to all the
40 | processes dying, measures how often it happens, and stops the incriminated
41 | watchers after too many restarts attempts.
42 | 
43 | Circus comes with a plugin system to help you write such extensions, and
44 | a few built-in plugins you can reuse. See :ref:`plugins`.
45 | 
46 | You can also have a more subtile startup and shutdown behavior by using the
47 | **hooks** system that will let you run arbitrary code before and after
48 | some processes are started or stopped. See :ref:`hooks`.
49 | 
50 | Last but not least, you can also add new commands. See :ref:`addingcmds`.
51 | 
52 | 
53 | Developers Documentation Index
54 | ------------------------------
55 | 
56 | .. toctree::
57 |    :maxdepth: 1
58 | 
59 |    library
60 |    writing-plugins
61 |    writing-hooks
62 |    adding-commands
63 | 


--------------------------------------------------------------------------------
/docs/source/installation.rst:
--------------------------------------------------------------------------------
 1 | .. _installation:
 2 | 
 3 | Installing Circus
 4 | #################
 5 | 
 6 | Circus is a Python package which is published on PyPI - the Python Package Index.
 7 | 
 8 | The simplest way to install it is to use pip, a tool for installing and managing Python packages::
 9 | 
10 |     $ pip install circus
11 | 
12 | Or download the `archive on PyPI `_,
13 | extract and install it manually with::
14 | 
15 |     $ python setup.py install
16 | 
17 | If you want to try out Circus, see the :ref:`examples`.
18 | 
19 | If you are using debian or any debian based distribution, you also can use the
20 | ppa to install circus, it's at
21 | https://launchpad.net/~roman-imankulov/+archive/circus
22 | 
23 | 
24 | zc.buildout
25 | ===========
26 | 
27 | We provide a `zc.buildout `_ configuration, you can
28 | use it by simply running the bootstrap script, then calling buildout::
29 | 
30 |     $ python bootstrap.py
31 |     $ bin/buildout
32 | 
33 | 
34 | More on Requirements
35 | ====================
36 | 
37 | Circus works with:
38 | 
39 | - Python 2.7, 3.2 or 3.3
40 | - zeromq >= 2.1.10 
41 |     - The version of zeromq supported is ultimately determined by what version of `pyzmq `_ is installed by pip during circus installation.
42 |     - Their current release supports 2.x (limited), 3.x, and 4.x ZeroMQ versions.
43 |     - **Note**: If you are using PyPy instead of CPython, make sure to read their installation docs as ZeroMQ version support is not the same on PyPy.
44 | 
45 | When you install circus, the latest
46 | versions of the Python dependencies will be pulled out for you.
47 | 
48 | You can also install them manually using the pip-requirements.txt
49 | file we provide::
50 | 
51 |     $ pip install -r pip-requirements.txt
52 | 
53 | 
54 | If you want to run the Web console you will need to install **circus-web**::
55 | 
56 |     $ pip install circus-web
57 | 


--------------------------------------------------------------------------------
/circus/commands/listsockets.py:
--------------------------------------------------------------------------------
 1 | from circus.commands.base import Command
 2 | import operator
 3 | 
 4 | 
 5 | class ListSockets(Command):
 6 |     """\
 7 |         Get the list of sockets
 8 |         =======================
 9 | 
10 |         ZMQ Message
11 |         -----------
12 | 
13 | 
14 |         To get the list of sockets::
15 | 
16 |             {
17 |                 "command": "listsockets",
18 |             }
19 | 
20 | 
21 |         The response return a list of json mappings with keys for fd, name,
22 |         host and port.
23 | 
24 |         Command line
25 |         ------------
26 | 
27 |         ::
28 | 
29 |             $ circusctl listsockets
30 |     """
31 |     name = "listsockets"
32 | 
33 |     def message(self, *args, **opts):
34 |         return self.make_message()
35 | 
36 |     def execute(self, arbiter, props):
37 | 
38 |         def _get_info(socket):
39 |             sock = {'fd': socket.fileno(),
40 |                     'name': socket.name,
41 |                     'backlog': socket.backlog}
42 | 
43 |             if socket.host is not None:
44 |                 sock['host'] = socket.host
45 |                 sock['port'] = socket.port
46 |             else:
47 |                 sock['path'] = socket.path
48 | 
49 |             return sock
50 | 
51 |         sockets = [_get_info(socket) for socket in arbiter.sockets.values()]
52 |         sockets.sort(key=operator.itemgetter('fd'))
53 |         return {"sockets": sockets}
54 | 
55 |     def console_msg(self, msg):
56 |         if 'sockets' in msg:
57 |             sockets = []
58 |             for sock in msg['sockets']:
59 |                 d = "%(fd)d:socket '%(name)s' "
60 |                 if 'path' in sock:
61 |                     d = (d + 'at %(path)s') % sock
62 |                 else:
63 |                     d = (d + 'at %(host)s:%(port)d') % sock
64 | 
65 |                 sockets.append(d)
66 | 
67 |             return "\n".join(sockets)
68 | 
69 |         return self.console_error(msg)
70 | 


--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
 1 | name: ci
 2 | 
 3 | on: [push, pull_request]
 4 | 
 5 | jobs:
 6 | 
 7 |   ubuntu:
 8 |     runs-on: ubuntu-20.04
 9 |     timeout-minutes: 10
10 | 
11 |     strategy:
12 |       fail-fast: false
13 |       matrix:
14 |         python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.7', 'pypy-3.8', 'pypy-3.9']
15 | 
16 |     steps:
17 |     - uses: actions/checkout@v3
18 | 
19 |     - name: Set up Python ${{ matrix.python-version }}
20 |       uses: actions/setup-python@v4
21 |       with:
22 |         python-version: ${{ matrix.python-version }}
23 | 
24 |     - name: Install system dependencies
25 |       run: |
26 |         sudo rm -f /etc/apt/sources.list.d/dotnetdev.list /etc/apt/sources.list.d/microsoft-prod.list
27 |         sudo apt update
28 |         sudo apt install libev-dev libevent-dev
29 |         sudo apt install gcc make libffi-dev pkg-config zlib1g-dev libbz2-dev libsqlite3-dev libncurses5-dev libexpat1-dev libssl-dev libgdbm-dev tk-dev libgc-dev python-cffi liblzma-dev libncursesw5-dev
30 |         sudo ldconfig
31 |     - name: Install test dependencies
32 |       run: pip install tox coveralls
33 |     - name: Run test suite
34 |       run: tox -v -e py
35 | 
36 | 
37 |   multi-os:
38 |     # Run tests for one python version on different operating systems
39 | 
40 |     runs-on: ${{ matrix.os }}
41 |     timeout-minutes: 15
42 | 
43 |     strategy:
44 |       matrix:
45 |         os: ['macos-12']
46 |         skip-tests: [false]
47 |         include:
48 |           - os: windows-2019
49 |             skip-tests: true
50 | 
51 |     steps:
52 |     - uses: actions/checkout@v3
53 | 
54 |     - name: Set up Python 3.8
55 |       uses: actions/setup-python@v4
56 |       with:
57 |         python-version: '3.8'
58 | 
59 |     - name: Install test dependencies
60 |       run: |
61 |         pip install tox coveralls
62 | 
63 |     - name: Run test suite
64 |       shell: bash -l {0}
65 |       run: tox -v -e py
66 |       continue-on-error: ${{ matrix.skip-tests }}
67 | 


--------------------------------------------------------------------------------
/circus/commands/numprocesses.py:
--------------------------------------------------------------------------------
 1 | from circus.commands.base import Command
 2 | from circus.exc import ArgumentError
 3 | 
 4 | 
 5 | class NumProcesses(Command):
 6 |     """\
 7 |         Get the number of processes
 8 |         ===========================
 9 | 
10 |         Get the number of processes in a watcher or in a arbiter
11 | 
12 |         ZMQ Message
13 |         -----------
14 | 
15 |         ::
16 | 
17 |             {
18 |                 "command": "numprocesses",
19 |                 "propeties": {
20 |                     "name": ""
21 |                 }
22 | 
23 |             }
24 | 
25 |         The response return the number of processes in the 'numprocesses`
26 |         property::
27 | 
28 |             { "status": "ok", "numprocesses": , "time", "timestamp" }
29 | 
30 |         If the property name isn't specified, the sum of all processes
31 |         managed is returned.
32 | 
33 |         Command line
34 |         ------------
35 | 
36 |         ::
37 | 
38 |             $ circusctl numprocesses []
39 | 
40 |         Options
41 |         +++++++
42 | 
43 |         - : name of the watcher
44 | 
45 |     """
46 |     name = "numprocesses"
47 | 
48 |     def message(self, *args, **opts):
49 |         if len(args) > 1:
50 |             raise ArgumentError("message invalid")
51 | 
52 |         if len(args) == 1:
53 |             return self.make_message(name=args[0])
54 |         else:
55 |             return self.make_message()
56 | 
57 |     def execute(self, arbiter, props):
58 |         if 'name' in props:
59 |             watcher = self._get_watcher(arbiter, props['name'])
60 |             return {
61 |                 "numprocesses": len(watcher),
62 |                 "watcher_name": props['name']
63 |             }
64 |         else:
65 |             return {"numprocesses": arbiter.numprocesses()}
66 | 
67 |     def console_msg(self, msg):
68 |         if msg.get("status") == "ok":
69 |             return str(msg.get("numprocesses"))
70 |         return self.console_error(msg)
71 | 


--------------------------------------------------------------------------------
/tests/test_convert_option.py:
--------------------------------------------------------------------------------
 1 | from tests.support import TestCase
 2 | 
 3 | from circus.commands.util import convert_option, ArgumentError
 4 | 
 5 | 
 6 | class TestConvertOption(TestCase):
 7 | 
 8 |     def test_env(self):
 9 |         env = convert_option("env", {"port": "8080"})
10 |         self.assertDictEqual({"port": "8080"}, env)
11 | 
12 |     def test_stdout_and_stderr_stream(self):
13 |         expected_convertions = (
14 |             ('stdout_stream.class', 'class', 'class'),
15 |             ('stdout_stream.filename', 'file', 'file'),
16 |             ('stdout_stream.other_option', 'other', 'other'),
17 |             ('stdout_stream.refresh_time', '10', '10'),
18 |             ('stdout_stream.max_bytes', '10', 10),
19 |             ('stdout_stream.backup_count', '20', 20),
20 |             ('stderr_stream.class', 'class', 'class'),
21 |             ('stderr_stream.filename', 'file', 'file'),
22 |             ('stderr_stream.other_option', 'other', 'other'),
23 |             ('stderr_stream.refresh_time', '10', '10'),
24 |             ('stderr_stream.max_bytes', '10', 10),
25 |             ('stderr_stream.backup_count', '20', 20),
26 |             ('stderr_stream.some_number', '99', '99'),
27 |             ('stderr_stream.some_number_2', 99, 99),
28 |         )
29 | 
30 |         for option, value, expected in expected_convertions:
31 |             ret = convert_option(option, value)
32 |             self.assertEqual(ret, expected)
33 | 
34 |     def test_hooks(self):
35 |         ret = convert_option('hooks', 'before_start:one')
36 |         self.assertEqual(ret, {'before_start': 'one'})
37 | 
38 |         ret = convert_option('hooks', 'before_start:one,after_start:two')
39 | 
40 |         self.assertEqual(ret['before_start'], 'one')
41 |         self.assertEqual(ret['after_start'], 'two')
42 | 
43 |         self.assertRaises(ArgumentError, convert_option, 'hooks',
44 |                           'before_start:one,DONTEXIST:two')
45 | 
46 |         self.assertRaises(ArgumentError, convert_option, 'hooks',
47 |                           'before_start:one:two')
48 | 
49 | 
50 | 


--------------------------------------------------------------------------------
/circus/commands/ipythonshell.py:
--------------------------------------------------------------------------------
 1 | import os
 2 | import sys
 3 | from circus.exc import ArgumentError
 4 | from circus.commands.base import Command
 5 | 
 6 | 
 7 | class IPythonShell(Command):
 8 |     """\
 9 |        Create shell into circusd process
10 |        =================================
11 | 
12 |        This command is only useful if you have the ipython package installed.
13 | 
14 |        Command Line
15 |        ------------
16 | 
17 |        ::
18 | 
19 |             $ circusctl ipython
20 | 
21 |     """
22 | 
23 |     name = "ipython"
24 | 
25 |     def message(self, *args, **opts):
26 |         if len(args) > 0:
27 |             raise ArgumentError("Invalid message")
28 |         return self.make_message()
29 | 
30 |     def execute(self, arbiter, props):
31 |         shell = 'kernel-%d.json' % os.getpid()
32 |         msg = None
33 |         try:
34 |             from IPython.kernel.zmq.kernelapp import IPKernelApp
35 |             if not IPKernelApp.initialized():
36 |                 app = IPKernelApp.instance()
37 |                 app.initialize([])
38 |                 main = app.kernel.shell._orig_sys_modules_main_mod
39 |                 if main is not None:
40 |                     sys.modules[
41 |                         app.kernel.shell._orig_sys_modules_main_name
42 |                     ] = main
43 |                 app.kernel.user_module = sys.modules[__name__]
44 |                 app.kernel.user_ns = {'arbiter': arbiter}
45 |                 app.shell.set_completer_frame()
46 |                 app.kernel.start()
47 | 
48 |         except Exception as e:
49 |             shell = False
50 |             msg = str(e)
51 | 
52 |         return {'shell': shell, 'msg': msg}
53 | 
54 |     def console_msg(self, msg):
55 |         if msg['status'] == "ok":
56 |             shell = msg['shell']
57 |             if shell:
58 |                 from IPython import start_ipython
59 |                 start_ipython(['console', '--existing', shell])
60 |                 return ''
61 |             else:
62 |                 msg['reason'] = 'Could not start ipython kernel'
63 |         return self.console_error(msg)
64 | 


--------------------------------------------------------------------------------
/circus/plugins/command_reloader.py:
--------------------------------------------------------------------------------
 1 | import os
 2 | 
 3 | from circus.plugins import CircusPlugin
 4 | from circus import logger
 5 | from circus.util import AsyncPeriodicCallback
 6 | 
 7 | 
 8 | class CommandReloader(CircusPlugin):
 9 | 
10 |     name = 'command_reloader'
11 | 
12 |     def __init__(self, *args, **config):
13 |         super(CommandReloader, self).__init__(*args, **config)
14 |         self.name = config.get('name')
15 |         self.loop_rate = int(self.config.get('loop_rate', 1))
16 |         self.cmd_files = {}
17 | 
18 |     def is_modified(self, watcher, current_mtime, current_path):
19 |         if watcher not in self.cmd_files:
20 |             return False
21 |         if current_mtime != self.cmd_files[watcher]['mtime']:
22 |             return True
23 |         if current_path != self.cmd_files[watcher]['path']:
24 |             return True
25 |         return False
26 | 
27 |     def look_after(self):
28 |         list_ = self.call('list')
29 |         watchers = [watcher for watcher in list_['watchers']
30 |                     if not watcher.startswith('plugin:')]
31 | 
32 |         for watcher in list(self.cmd_files.keys()):
33 |             if watcher not in watchers:
34 |                 del self.cmd_files[watcher]
35 | 
36 |         for watcher in watchers:
37 |             watcher_info = self.call('get', name=watcher, keys=['cmd'])
38 |             cmd = watcher_info['options']['cmd']
39 |             cmd_path = os.path.realpath(cmd)
40 |             cmd_mtime = os.stat(cmd_path).st_mtime
41 |             if self.is_modified(watcher, cmd_mtime, cmd_path):
42 |                 logger.info('%s modified. Restarting.', cmd_path)
43 |                 self.call('restart', name=watcher)
44 |             self.cmd_files[watcher] = {
45 |                 'path': cmd_path,
46 |                 'mtime': cmd_mtime,
47 |             }
48 | 
49 |     def handle_init(self):
50 |         self.period = AsyncPeriodicCallback(self.look_after,
51 |                                             self.loop_rate * 1000)
52 |         self.period.start()
53 | 
54 |     def handle_stop(self):
55 |         self.period.stop()
56 | 
57 |     def handle_recv(self, data):
58 |         pass
59 | 


--------------------------------------------------------------------------------
/circus/commands/listen.py:
--------------------------------------------------------------------------------
 1 | from circus.commands.base import Command
 2 | from circus.exc import MessageError
 3 | 
 4 | 
 5 | class Listen(Command):
 6 |     """\
 7 |         Subscribe to a watcher event
 8 |         ============================
 9 | 
10 |         ZMQ
11 |         ---
12 | 
13 |         At any moment you can subscribe to a circus event. Circus provides
14 |         a PUB/SUB feed on which any clients can subscribe. The subscriber
15 |         endpoint URI is set in the circus.ini configuration file.
16 | 
17 |         Events are pubsub topics:
18 | 
19 |         - `watcher..reap`: when a process is reaped
20 |         - `watcher..spawn`: when a process is spawned
21 |         - `watcher..kill`: when a process is killed
22 |         - `watcher..updated`: when watcher configuration
23 |           is updated
24 |         - `watcher..stop`: when a watcher is stopped
25 |         - `watcher..start`: when a watcher is started
26 | 
27 |         All events messages are in a json struct.
28 | 
29 |         Command line
30 |         ------------
31 | 
32 |         The client has been updated to provide a simple way to listen on the
33 |         events::
34 | 
35 |             circusctl listen [, ...]
36 | 
37 |         Example of result:
38 |         ++++++++++++++++++
39 | 
40 |         ::
41 | 
42 |             $ circusctl listen tcp://127.0.0.1:5556
43 |             watcher.refuge.spawn: {u'process_id': 6, u'process_pid': 72976,
44 |                                    u'time': 1331681080.985104}
45 |             watcher.refuge.spawn: {u'process_id': 7, u'process_pid': 72995,
46 |                                    u'time': 1331681086.208542}
47 |             watcher.refuge.spawn: {u'process_id': 8, u'process_pid': 73014,
48 |                                    u'time': 1331681091.427005}
49 |     """
50 |     name = "listen"
51 |     msg_type = "sub"
52 | 
53 |     def message(self, *args, **opts):
54 |         if not args:
55 |             return [""]
56 |         return list(args)
57 | 
58 |     def execute(self, arbiter, args):
59 |         raise MessageError("invalid message. use a pub/sub socket")
60 | 


--------------------------------------------------------------------------------
/docs/source/design/architecture.rst:
--------------------------------------------------------------------------------
 1 | .. _design:
 2 | 
 3 | Overall architecture
 4 | ####################
 5 | 
 6 | .. image:: circus-architecture.png
 7 |    :align: center
 8 | 
 9 | Circus is composed of a main process called **circusd** which takes
10 | care of running all the processes. Each process managed by Circus
11 | is a child process of **circusd**.
12 | 
13 | Processes are organized in groups called **watchers**. A
14 | **watcher** is basically a command **circusd** runs on your system,
15 | and for each command you can configure how many processes you
16 | want to run.
17 | 
18 | The concept of *watcher* is useful when you want to manage all the
19 | processes running the same command -- like restart them, etc.
20 | 
21 | **circusd** binds two ZeroMQ sockets:
22 | 
23 | - **REQ/REP** -- a socket used to control **circusd** using json-based
24 |   *commands*.
25 | - **PUB/SUB** -- a socket where **circusd** publishes events, like
26 |   when a process is started or stopped.
27 | 
28 | .. note::
29 | 
30 |    Despite its name, ZeroMQ is not a queue management system. Think of it
31 |    as an inter-process communication (IPC) library.
32 | 
33 | Another process called **circusd-stats** is run by **circusd** when
34 | the option is activated. **circusd-stats**'s job is to publish
35 | CPU/Memory usage statistics in a dedicated **PUB/SUB** channel.
36 | 
37 | This specialized channel is used by **circus-top** and
38 | **circus-httpd** to display a live stream of the activity.
39 | 
40 | **circus-top** is a console script that mimics **top** to display
41 | all the CPU and Memory usage of the processes managed by Circus.
42 | 
43 | **circus-httpd** is the web managment interface that will let you
44 | interact with Circus. It displays a live stream using web sockets
45 | and the **circusd-stats** channel, but also let you interact with
46 | **circusd** via its **REQ/REP** channel.
47 | 
48 | Last but not least, **circusctl** is a command-line tool that let
49 | you drive **circusd** via its **REQ/REP** channel.
50 | 
51 | You can also have plugins that subscribe to **circusd**'s **PUB/SUB**
52 | channel and let you send commands to the **REQ/REP** channel like
53 | **circusctl** would.
54 | 


--------------------------------------------------------------------------------
/docs/source/man/circusctl.rst:
--------------------------------------------------------------------------------
 1 | circusctl man page
 2 | ##################
 3 | 
 4 | Synopsis
 5 | --------
 6 | 
 7 | circusctl [options] command [args]
 8 | 
 9 | 
10 | Description
11 | -----------
12 | 
13 | circusctl is front end to control the Circus daemon. It is designed to
14 | help the administrator control the functionning of the Circud
15 | **circusd** daemon.
16 | 
17 | 
18 | Commands
19 | --------
20 | 
21 | :add: Add a watcher
22 | :decr: Decrement the number of processes in a watcher
23 | :dstats: Get circusd stats
24 | :get: Get the value of specific watcher options
25 | :globaloptions: Get the arbiter options
26 | :incr: Increment the number of processes in a watcher
27 | :ipython: Create shell into circusd process
28 | :list: Get list of watchers or processes in a watcher
29 | :listen: Subscribe to a watcher event
30 | :listsockets: Get the list of sockets
31 | :numprocesses: Get the number of processes
32 | :numwatchers: Get the number of watchers
33 | :options: Get the value of all options for a watcher
34 | :quit: Quit the arbiter immediately
35 | :reload: Reload the arbiter or a watcher
36 | :reloadconfig: Reload the configuration file
37 | :restart: Restart the arbiter or a watcher
38 | :rm: Remove a watcher
39 | :set: Set a watcher option
40 | :signal: Send a signal
41 | :start: Start the arbiter or a watcher
42 | :stats: Get process infos
43 | :status: Get the status of a watcher or all watchers
44 | :stop: Stop watchers
45 | 
46 | 
47 | Options
48 | -------
49 | 
50 | :--endpoint *ENDPOINT*:
51 |    connection endpoint
52 | 
53 | :-h, \--help:
54 |    Show the help message and exit
55 | 
56 | :--json:
57 |    output to JSON
58 | 
59 | :--prettify:
60 |    prettify output
61 | 
62 | :--ssh *SSH*:
63 |    SSH Server in the format ``user@host:port``
64 | 
65 | :--ssh_keyfile *SSH_KEYFILE*:
66 |    path to the keyfile to authorise the user
67 | 
68 | :--timeout *TIMEOUT*:
69 |    connection timeout
70 | 
71 | :\--version:
72 |    Displays Circus version and exits.
73 | 
74 | 
75 | See Also
76 | --------
77 | 
78 | `circus` (1), `circusd` (1), `circusd-stats` (1), `circus-plugin` (1), `circus-top` (1).
79 | 
80 | Full Documentation is available at https://circus.readthedocs.io
81 | 


--------------------------------------------------------------------------------
/circus/consumer.py:
--------------------------------------------------------------------------------
 1 | import errno
 2 | import zmq
 3 | 
 4 | from circus.util import DEFAULT_ENDPOINT_SUB, get_connection, to_bytes
 5 | 
 6 | 
 7 | class CircusConsumer(object):
 8 |     def __init__(self, topics, context=None, endpoint=DEFAULT_ENDPOINT_SUB,
 9 |                  ssh_server=None, timeout=1.):
10 |         self.topics = topics
11 |         self.keep_context = context is not None
12 |         self._init_context(context)
13 |         self.endpoint = endpoint
14 |         self.pubsub_socket = self.context.socket(zmq.SUB)
15 |         get_connection(self.pubsub_socket, self.endpoint, ssh_server)
16 |         for topic in self.topics:
17 |             self.pubsub_socket.setsockopt(zmq.SUBSCRIBE, to_bytes(topic))
18 |         self._init_poller()
19 |         self.timeout = timeout
20 | 
21 |     def __enter__(self):
22 |         return self
23 | 
24 |     def __exit__(self, exc_type, exc_value, traceback):
25 |         """ On context manager exit, destroy the zmq context """
26 |         self.stop()
27 | 
28 |     def __iter__(self):
29 |         return self.iter_messages()
30 | 
31 |     def _init_context(self, context):
32 |         self.context = context or zmq.Context()
33 | 
34 |     def _init_poller(self):
35 |         self.poller = zmq.Poller()
36 |         self.poller.register(self.pubsub_socket, zmq.POLLIN)
37 | 
38 |     def iter_messages(self):
39 |         """ Yields tuples of (topic, message) """
40 |         with self:
41 |             while True:
42 |                 try:
43 |                     events = dict(self.poller.poll(self.timeout * 1000))
44 |                 except zmq.ZMQError as e:
45 |                     if e.errno == errno.EINTR:
46 |                         continue
47 |                     raise
48 | 
49 |                 if len(events) == 0:
50 |                     continue
51 | 
52 |                 topic, message = self.pubsub_socket.recv_multipart()
53 |                 yield topic, message
54 | 
55 |     def stop(self):
56 |         if self.keep_context:
57 |             return
58 |         try:
59 |             self.context.destroy(0)
60 |         except zmq.ZMQError as e:
61 |             if e.errno == errno.EINTR:
62 |                 pass
63 |             else:
64 |                 raise
65 | 


--------------------------------------------------------------------------------
/circus/commands/status.py:
--------------------------------------------------------------------------------
 1 | from circus.commands.base import Command
 2 | from circus.exc import ArgumentError
 3 | 
 4 | 
 5 | class Status(Command):
 6 |     """\
 7 |         Get the status of a watcher or all watchers
 8 |         ===========================================
 9 | 
10 |         This command start get the status of a watcher or all watchers.
11 | 
12 |         ZMQ Message
13 |         -----------
14 | 
15 |         ::
16 | 
17 |             {
18 |                 "command": "status",
19 |                 "properties": {
20 |                     "name": '",
21 |                 }
22 |             }
23 | 
24 |         The response return the status "active" or "stopped" or the
25 |         status / watchers.
26 | 
27 | 
28 |         Command line
29 |         ------------
30 | 
31 |         ::
32 | 
33 |             $ circusctl status []
34 | 
35 |         Options
36 |         +++++++
37 | 
38 |         - : name of the watcher
39 | 
40 |         Example
41 |         +++++++
42 | 
43 |         ::
44 | 
45 |             $ circusctl status dummy
46 |             active
47 |             $ circusctl status
48 |             dummy: active
49 |             dummy2: active
50 |             refuge: active
51 | 
52 |     """
53 | 
54 |     name = "status"
55 | 
56 |     def message(self, *args, **opts):
57 |         if len(args) > 1:
58 |             raise ArgumentError("message invalid")
59 | 
60 |         if len(args) == 1:
61 |             return self.make_message(name=args[0])
62 |         else:
63 |             return self.make_message()
64 | 
65 |     def execute(self, arbiter, props):
66 |         if 'name' in props:
67 |             watcher = self._get_watcher(arbiter, props['name'])
68 |             return {"status": watcher.status()}
69 |         else:
70 |             return {"statuses": arbiter.statuses()}
71 | 
72 |     def console_msg(self, msg):
73 |         if "statuses" in msg:
74 |             statuses = msg.get("statuses")
75 |             watchers = sorted(statuses)
76 |             return "\n".join(["%s: %s" % (watcher, statuses[watcher])
77 |                               for watcher in watchers])
78 |         elif "status" in msg and "status" != "error":
79 |             return msg.get("status")
80 |         return self.console_error(msg)
81 | 


--------------------------------------------------------------------------------
/circus/commands/dstats.py:
--------------------------------------------------------------------------------
 1 | from circus.exc import ArgumentError
 2 | from circus.commands.base import Command
 3 | from circus.util import get_info
 4 | 
 5 | _INFOLINE = ("%(pid)s  %(cmdline)s %(username)s %(nice)s %(mem_info1)s "
 6 |              "%(mem_info2)s %(cpu)s %(mem)s %(ctime)s")
 7 | 
 8 | 
 9 | class Daemontats(Command):
10 |     """\
11 |        Get circusd stats
12 |        =================
13 | 
14 |        You can get at any time some statistics about circusd
15 |        with the dstat command.
16 | 
17 |        ZMQ Message
18 |        -----------
19 | 
20 |        To get the circusd stats, simply run::
21 | 
22 |             {
23 |                 "command": "dstats"
24 |             }
25 | 
26 | 
27 |        The response returns a mapping the property "infos"
28 |        containing some process informations::
29 | 
30 |             {
31 |               "info": {
32 |                 "children": [],
33 |                 "cmdline": "python",
34 |                 "cpu": 0.1,
35 |                 "ctime": "0:00.41",
36 |                 "mem": 0.1,
37 |                 "mem_info1": "3M",
38 |                 "mem_info2": "2G",
39 |                 "nice": 0,
40 |                 "pid": 47864,
41 |                 "username": "root"
42 |               },
43 |               "status": "ok",
44 |               "time": 1332265655.897085
45 |             }
46 | 
47 |        Command Line
48 |        ------------
49 | 
50 |        ::
51 | 
52 |             $ circusctl dstats
53 | 
54 |     """
55 | 
56 |     name = "dstats"
57 | 
58 |     def message(self, *args, **opts):
59 |         if len(args) > 0:
60 |             raise ArgumentError("Invalid message")
61 |         return self.make_message()
62 | 
63 |     def execute(self, arbiter, props):
64 |         return {'info': get_info(interval=0.01)}
65 | 
66 |     def _to_str(self, info):
67 |         children = info.pop("children", [])
68 |         ret = ['Main Process:',  '    ' + _INFOLINE % info]
69 | 
70 |         if len(children) > 0:
71 |             ret.append('Children:')
72 |             for child in children:
73 |                 ret.append('    ' + _INFOLINE % child)
74 | 
75 |         return "\n".join(ret)
76 | 
77 |     def console_msg(self, msg):
78 |         if msg['status'] == "ok":
79 |             return self._to_str(msg['info'])
80 |         else:
81 |             return self.console_error(msg)
82 | 


--------------------------------------------------------------------------------
/circus/commands/stop.py:
--------------------------------------------------------------------------------
 1 | from circus.commands.base import Command
 2 | from circus.commands.restart import execute_watcher_start_stop_restart
 3 | from circus.commands.restart import match_options
 4 | 
 5 | 
 6 | class Stop(Command):
 7 |     """\
 8 |         Stop watchers
 9 |         =============
10 | 
11 |         This command stops a given watcher or all watchers.
12 | 
13 |         ZMQ Message
14 |         -----------
15 | 
16 |         ::
17 | 
18 |             {
19 |                 "command": "stop",
20 |                 "properties": {
21 |                     "name": "",
22 |                     "waiting": False,
23 |                     "match": "[simple|glob|regex]"
24 |                 }
25 |             }
26 | 
27 |         The response returns the status "ok".
28 | 
29 |         If the ``name`` property is present, then the stop will be applied
30 |         to the watcher corresponding to that name. Otherwise, all watchers
31 |         will get stopped.
32 | 
33 |         If ``waiting`` is False (default), the call will return immediatly
34 |         after calling `stop_signal` on each process.
35 | 
36 |         If ``waiting`` is True, the call will return only when the stop process
37 |         is completly ended. Because of the
38 |         :ref:`graceful_timeout option `, it can take some
39 |         time.
40 | 
41 |         The ``match`` parameter can have the value ``simple`` for string
42 |         compare, ``glob`` for wildcard matching (default) or ``regex`` for
43 |         regex matching.
44 | 
45 | 
46 |         Command line
47 |         ------------
48 | 
49 |         ::
50 | 
51 |             $ circusctl stop [name] [--waiting] [--match=simple|glob|regex]
52 | 
53 |         Options
54 |         +++++++
55 | 
56 |         - : name or pattern of the watcher(s)
57 |         - : watcher match method
58 |     """
59 | 
60 |     name = "stop"
61 |     options = list(Command.waiting_options)
62 |     options.append(match_options)
63 | 
64 |     def message(self, *args, **opts):
65 |         if len(args) >= 1:
66 |             return self.make_message(name=args[0], **opts)
67 |         return self.make_message(**opts)
68 | 
69 |     def execute(self, arbiter, props):
70 |         return execute_watcher_start_stop_restart(
71 |             self, arbiter, props, 'stop', arbiter.stop_watchers,
72 |             arbiter.stop_watchers)
73 | 


--------------------------------------------------------------------------------
/tests/test_command_set.py:
--------------------------------------------------------------------------------
 1 | from tests.support import TestCircus
 2 | from tests.test_command_incrproc import FakeArbiter as _FakeArbiter
 3 | from circus.commands.set import Set
 4 | 
 5 | 
 6 | class FakeWatcher(object):
 7 |     def __init__(self):
 8 |         self.actions = []
 9 |         self.options = {}
10 | 
11 |     def set_opt(self, key, val):
12 |         self.options[key] = val
13 | 
14 |     def do_action(self, action):
15 |         self.actions.append(action)
16 | 
17 | 
18 | class FakeArbiter(_FakeArbiter):
19 |     watcher_class = FakeWatcher
20 | 
21 | 
22 | class SetTest(TestCircus):
23 | 
24 |     def test_set_stream(self):
25 |         arbiter = FakeArbiter()
26 |         cmd = Set()
27 | 
28 |         # setting streams
29 |         props = cmd.message('dummy', 'stdout_stream.class', 'FileStream')
30 |         props = props['properties']
31 |         cmd.execute(arbiter, props)
32 |         watcher = arbiter.watchers[0]
33 |         self.assertEqual(watcher.options,
34 |                          {'stdout_stream.class': 'FileStream'})
35 |         self.assertEqual(watcher.actions, [0])
36 | 
37 |         # setting hooks
38 |         props = cmd.message('dummy', 'hooks.before_start', 'some.hook')
39 |         props = props['properties']
40 |         cmd.execute(arbiter, props)
41 |         watcher = arbiter.watchers[0]
42 |         self.assertEqual(watcher.options['hooks.before_start'],
43 |                          'some.hook')
44 |         self.assertEqual(watcher.actions, [0, 0])
45 | 
46 |         # we can also set several hooks at once
47 |         props = cmd.message('dummy', 'hooks',
48 |                             'before_start:some,after_start:hook')
49 |         props = props['properties']
50 |         cmd.execute(arbiter, props)
51 |         watcher = arbiter.watchers[0]
52 |         self.assertEqual(watcher.options['hooks.before_start'],
53 |                          'some')
54 |         self.assertEqual(watcher.options['hooks.after_start'],
55 |                          'hook')
56 | 
57 |     def test_set_args(self):
58 |         arbiter = FakeArbiter()
59 |         cmd = Set()
60 | 
61 |         props = cmd.message('dummy2', 'args', '--arg1 1 --arg2 2')
62 |         props = props['properties']
63 |         cmd.execute(arbiter, props)
64 |         watcher = arbiter.watchers[0]
65 |         self.assertEqual(watcher.options['args'], '--arg1 1 --arg2 2')
66 | 
67 | 
68 | 


--------------------------------------------------------------------------------
/tests/test_command_stats.py:
--------------------------------------------------------------------------------
 1 | from tests.support import TestCircus
 2 | from circus.commands.stats import Stats, MessageError
 3 | 
 4 | 
 5 | _WANTED = """\
 6 | foo:
 7 | one: 1233  xx tarek false 132 132 13 123 xx
 8 |    1233  xx tarek false 132 132 13 123 xx
 9 |    1233  xx tarek false 132 132 13 123 xx"""
10 | 
11 | 
12 | class FakeWatcher(object):
13 |     name = 'one'
14 | 
15 |     def info(self, *args):
16 |         if len(args) == 2 and args[0] == 'meh':
17 |             raise KeyError('meh')
18 |         return 'yeah'
19 | 
20 |     process_info = info
21 | 
22 | 
23 | class FakeArbiter(object):
24 |     watchers = [FakeWatcher()]
25 | 
26 |     def get_watcher(self, name):
27 |         return FakeWatcher()
28 | 
29 | 
30 | class StatsCommandTest(TestCircus):
31 | 
32 |     def test_console_msg(self):
33 |         cmd = Stats()
34 |         info = {'pid': '1233',
35 |                 'cmdline': 'xx',
36 |                 'username': 'tarek',
37 |                 'nice': 'false',
38 |                 'mem_info1': '132',
39 |                 'mem_info2': '132',
40 |                 'cpu': '13',
41 |                 'mem': '123',
42 |                 'ctime': 'xx'}
43 | 
44 |         info['children'] = [dict(info), dict(info)]
45 | 
46 |         res = cmd.console_msg({'name': 'foo',
47 |                                'status': 'ok',
48 |                                'info': {'one': info}})
49 | 
50 |         self.assertEqual(res, _WANTED)
51 | 
52 |     def test_execute(self):
53 |         cmd = Stats()
54 |         arbiter = FakeArbiter()
55 |         res = cmd.execute(arbiter, {})
56 |         self.assertEqual({'infos': {'one': 'yeah'}}, res)
57 | 
58 |         # info about a specific watcher
59 |         props = {'name': 'one'}
60 |         res = cmd.execute(arbiter, props)
61 |         res = sorted(res.items())
62 |         wanted = [('info', 'yeah'), ('name', 'one')]
63 |         self.assertEqual(wanted, res)
64 | 
65 |         # info about a specific process
66 |         props = {'process': '123', 'name': 'one'}
67 |         res = cmd.execute(arbiter, props)
68 |         res = sorted(res.items())
69 |         wanted = [('info', 'yeah'), ('process', '123')]
70 |         self.assertEqual(wanted, res)
71 | 
72 |         # info that breaks
73 |         props = {'name': 'meh', 'process': 'meh'}
74 |         self.assertRaises(MessageError, cmd.execute, arbiter, props)
75 | 
76 | 
77 | 


--------------------------------------------------------------------------------
/circus/commands/list.py:
--------------------------------------------------------------------------------
 1 | from circus.commands.base import Command
 2 | from circus.exc import ArgumentError
 3 | 
 4 | from circus import logger
 5 | 
 6 | 
 7 | class List(Command):
 8 |     """\
 9 |         Get list of watchers or processes in a watcher
10 |         ==============================================
11 | 
12 |         ZMQ Message
13 |         -----------
14 | 
15 | 
16 |         To get the list of all the watchers::
17 | 
18 |             {
19 |                 "command": "list",
20 |             }
21 | 
22 | 
23 |         To get the list of active processes in a watcher::
24 | 
25 |             {
26 |                 "command": "list",
27 |                 "properties": {
28 |                     "name": "nameofwatcher",
29 |                 }
30 |             }
31 | 
32 | 
33 |         The response return the list asked. the mapping returned can either be
34 |         'watchers' or 'pids' depending the request.
35 | 
36 |         Command line
37 |         ------------
38 | 
39 |         ::
40 | 
41 |             $ circusctl list []
42 |     """
43 |     name = "list"
44 | 
45 |     def message(self, *args, **opts):
46 |         if len(args) > 1:
47 |             raise ArgumentError("Invalid number of arguments")
48 | 
49 |         if len(args) == 1:
50 |             return self.make_message(name=args[0])
51 |         else:
52 |             return self.make_message()
53 | 
54 |     def execute(self, arbiter, props):
55 |         if 'name' in props:
56 |             watcher = self._get_watcher(arbiter, props['name'])
57 |             processes = watcher.get_active_processes()
58 |             status = [(p.pid, p.status) for p in processes]
59 |             logger.debug('here is the status of the processes %s' % status)
60 |             return {"pids":  [p.pid for p in processes]}
61 |         else:
62 |             watchers = sorted(arbiter._watchers_names)
63 |             return {"watchers": [name for name in watchers]}
64 | 
65 |     def console_msg(self, msg):
66 |         if "pids" in msg:
67 |             return ",".join([str(process_id)
68 |                              for process_id in msg.get('pids')])
69 |         elif 'watchers' in msg:
70 |             return ",".join([watcher for watcher in msg.get('watchers')])
71 |         if 'reason' not in msg:
72 |             msg['reason'] = "Response doesn't contain 'pids' nor 'watchers'."
73 |         return self.console_error(msg)
74 | 


--------------------------------------------------------------------------------
/circus/commands/start.py:
--------------------------------------------------------------------------------
 1 | from circus.commands.base import Command
 2 | from circus.commands.restart import execute_watcher_start_stop_restart
 3 | from circus.commands.restart import match_options
 4 | from circus.exc import ArgumentError
 5 | 
 6 | 
 7 | class Start(Command):
 8 |     """\
 9 |         Start the arbiter or a watcher
10 |         ==============================
11 | 
12 |         This command starts all the processes in a watcher or all watchers.
13 | 
14 | 
15 |         ZMQ Message
16 |         -----------
17 | 
18 |         ::
19 | 
20 |             {
21 |                 "command": "start",
22 |                 "properties": {
23 |                     "name": '",
24 |                     "waiting": False,
25 |                     "match": "[simple|glob|regex]"
26 |                 }
27 |             }
28 | 
29 |         The response return the status "ok".
30 | 
31 |         If the property name is present, the watcher will be started.
32 | 
33 |         If ``waiting`` is False (default), the call will return immediately
34 |         after calling `start` on each process.
35 | 
36 |         If ``waiting`` is True, the call will return only when the start
37 |         process is completely ended. Because of the
38 |         :ref:`graceful_timeout option `, it can take some
39 |         time.
40 | 
41 |         The ``match`` parameter can have the value ``simple`` for string
42 |         compare, ``glob`` for wildcard matching (default) or ``regex`` for
43 |         regex matching.
44 | 
45 | 
46 |         Command line
47 |         ------------
48 | 
49 |         ::
50 | 
51 |         $ circusctl restart [name] [--waiting] [--match=simple|glob|regex]
52 | 
53 |         Options
54 |         +++++++
55 | 
56 |         - : name or pattern of the watcher(s)
57 |         - : watcher match method
58 | 
59 |     """
60 |     name = "start"
61 |     options = list(Command.waiting_options)
62 |     options.append(match_options)
63 | 
64 |     def message(self, *args, **opts):
65 |         if len(args) > 1:
66 |             raise ArgumentError("Invalid number of arguments")
67 | 
68 |         if len(args) == 1:
69 |             return self.make_message(name=args[0], **opts)
70 | 
71 |         return self.make_message(**opts)
72 | 
73 |     def execute(self, arbiter, props):
74 |         return execute_watcher_start_stop_restart(
75 |             self, arbiter, props, 'start', arbiter.start_watchers,
76 |             arbiter.start_watchers)
77 | 


--------------------------------------------------------------------------------
/circus/commands/rmwatcher.py:
--------------------------------------------------------------------------------
 1 | from circus.commands.base import Command
 2 | from circus.exc import ArgumentError
 3 | 
 4 | 
 5 | class RmWatcher(Command):
 6 |     """\
 7 |         Remove a watcher
 8 |         ================
 9 | 
10 |         This command removes a watcher dynamically from the arbiter. The
11 |         watchers are gracefully stopped by default.
12 | 
13 |         ZMQ Message
14 |         -----------
15 | 
16 |         ::
17 | 
18 |             {
19 |                 "command": "rm",
20 |                 "properties": {
21 |                     "name": "",
22 |                     "nostop": False,
23 |                     "waiting": False
24 |                 }
25 |             }
26 | 
27 |         The response return a status "ok".
28 | 
29 |         If ``nostop`` is True (default: False), the processes for the watcher
30 |         will not be stopped - instead the watcher will just be forgotten by
31 |         circus and the watcher processes will be responsible for stopping
32 |         themselves. If ``nostop`` is not specified or is False, then the
33 |         watcher processes will be stopped gracefully.
34 | 
35 |         If ``waiting`` is False (default), the call will return immediately
36 |         after starting to remove and stop the corresponding watcher.
37 | 
38 |         If ``waiting`` is True, the call will return only when the remove and
39 |         stop process is completely ended. Because of the
40 |         :ref:`graceful_timeout option `, it can take some
41 |         time.
42 | 
43 | 
44 |         Command line
45 |         ------------
46 | 
47 |         ::
48 | 
49 |             $ circusctl rm  [--waiting] [--nostop]
50 | 
51 |         Options
52 |         +++++++
53 | 
54 |         - : name of the watcher to remove
55 |         - nostop: do not stop the watcher processes, just remove the watcher
56 | 
57 |     """
58 | 
59 |     name = "rm"
60 |     properties = ['name']
61 |     options = Command.waiting_options + \
62 |         [('nostop', 'nostop', False, 'Do not stop watcher processes')]
63 | 
64 |     def message(self, *args, **opts):
65 |         if len(args) < 1 or len(args) > 1:
66 |             raise ArgumentError("Invalid number of arguments")
67 | 
68 |         return self.make_message(name=args[0])
69 | 
70 |     def execute(self, arbiter, props):
71 |         self._get_watcher(arbiter, props['name'])
72 |         return arbiter.rm_watcher(props['name'], props.get('nostop', False))
73 | 


--------------------------------------------------------------------------------
/tests/test_command_incrproc.py:
--------------------------------------------------------------------------------
 1 | from tests.support import TestCircus
 2 | from circus.commands.incrproc import IncrProc
 3 | 
 4 | 
 5 | class FakeWatcher(object):
 6 |     name = 'one'
 7 |     singleton = False
 8 | 
 9 |     def __init__(self):
10 |         self.numprocesses = 1
11 | 
12 |     def info(self, *args):
13 |         if len(args) == 1 and args[0] == 'meh':
14 |             raise KeyError('meh')
15 |         return 'yeah'
16 | 
17 |     process_info = info
18 | 
19 |     def incr(self, nb):
20 |         self.numprocesses += nb
21 | 
22 |     def decr(self, nb):
23 |         self.numprocesses -= nb
24 | 
25 | 
26 | class FakeSingletonWatcher(FakeWatcher):
27 |     singleton = True
28 | 
29 | 
30 | class FakeLoop(object):
31 |     def add_callback(self, function):
32 |         function()
33 | 
34 | 
35 | class FakeArbiter(object):
36 | 
37 |     watcher_class = FakeWatcher
38 | 
39 |     def __init__(self):
40 |         self.watchers = [self.watcher_class()]
41 |         self.loop = FakeLoop()
42 | 
43 |     def get_watcher(self, name):
44 |         return self.watchers[0]
45 | 
46 |     def stop_watchers(self, **options):
47 |         self.watchers[:] = []
48 | 
49 |     def stop(self, **options):
50 |         self.stop_watchers(**options)
51 | 
52 | 
53 | class FakeArbiterWithSingletonWatchers(FakeArbiter):
54 |     watcher_class = FakeSingletonWatcher
55 | 
56 | 
57 | class IncrProcTest(TestCircus):
58 | 
59 |     def test_incr_proc_message(self):
60 |         cmd = IncrProc()
61 |         message = cmd.message('dummy')
62 |         self.assertTrue(message['properties'], {'name': 'dummy'})
63 | 
64 |         message = cmd.message('dummy', 3)
65 |         props = sorted(message['properties'].items())
66 |         self.assertEqual(props, [('name', 'dummy'), ('nb', 3)])
67 | 
68 |     def test_incr_proc(self):
69 |         cmd = IncrProc()
70 |         arbiter = FakeArbiter()
71 |         size_before = arbiter.watchers[0].numprocesses
72 | 
73 |         props = cmd.message('dummy', 3)['properties']
74 |         cmd.execute(arbiter, props)
75 |         self.assertEqual(arbiter.watchers[0].numprocesses, size_before + 3)
76 | 
77 |     def test_incr_proc_singleton(self):
78 |         cmd = IncrProc()
79 |         arbiter = FakeArbiterWithSingletonWatchers()
80 |         size_before = arbiter.watchers[0].numprocesses
81 | 
82 |         props = cmd.message('dummy', 3)['properties']
83 |         cmd.execute(arbiter, props)
84 |         self.assertEqual(arbiter.watchers[0].numprocesses, size_before)
85 | 
86 | 
87 | 


--------------------------------------------------------------------------------
/tests/test_pidfile.py:
--------------------------------------------------------------------------------
 1 | import tempfile
 2 | import os
 3 | import stat
 4 | import subprocess
 5 | 
 6 | from circus.pidfile import Pidfile
 7 | from tests.support import TestCase, SLEEP
 8 | 
 9 | 
10 | class TestPidfile(TestCase):
11 |     def test_pidfile(self):
12 |         proc = subprocess.Popen(SLEEP % 120, shell=True)
13 |         fd, path = tempfile.mkstemp()
14 |         os.close(fd)
15 | 
16 |         rf = path + '.2'
17 |         try:
18 |             pidfile = Pidfile(path)
19 | 
20 |             pidfile.create(proc.pid)
21 |             mode = os.stat(path).st_mode
22 |             self.assertEqual(stat.S_IMODE(mode), pidfile.perm_mode, path)
23 |             pidfile.unlink()
24 |             self.assertFalse(os.path.exists(path))
25 |             pidfile.create(proc.pid)
26 |             pidfile.rename(rf)
27 |             self.assertTrue(os.path.exists(rf))
28 |             self.assertFalse(os.path.exists(path))
29 |             mode = os.stat(rf).st_mode
30 |             self.assertEqual(stat.S_IMODE(mode), pidfile.perm_mode, rf)
31 |         finally:
32 |             os.remove(rf)
33 | 
34 |     def test_pidfile_data(self):
35 |         proc = subprocess.Popen(SLEEP % 120, shell=True)
36 |         fd, path = tempfile.mkstemp()
37 | 
38 |         os.write(fd, "fail-to-validate\n".encode('utf-8'))
39 |         os.close(fd)
40 | 
41 |         try:
42 |             pidfile = Pidfile(path)
43 | 
44 |             pidfile.create(proc.pid)
45 |             self.assertTrue(os.path.exists(path))
46 |             pid = 0
47 |             with open(path, "r") as f:
48 |                 pid = int(f.read() or 0)
49 |             self.assertEqual(pid, proc.pid)
50 |         finally:
51 |             os.remove(path)
52 | 
53 |         fd, path = tempfile.mkstemp()
54 | 
55 |         proc2 = subprocess.Popen(SLEEP % 0, shell=True)
56 | 
57 |         os.write(fd, "{0}\n".format(proc2.pid).encode('utf-8'))
58 |         os.close(fd)
59 | 
60 |         proc2.wait()
61 | 
62 |         try:
63 |             pidfile = Pidfile(path)
64 | 
65 |             self.assertNotEqual(proc2.pid, proc.pid)
66 |             pidfile.create(proc.pid)
67 |         finally:
68 |             os.remove(path)
69 | 
70 |         fd, path = tempfile.mkstemp()
71 | 
72 |         os.write(fd, "fail-to-int\n".encode('utf-8'))
73 |         os.close(fd)
74 | 
75 |         try:
76 |             pidfile = Pidfile(path)
77 | 
78 |             pidfile.unlink()
79 |             self.assertFalse(os.path.exists(path))
80 |         except Exception as e:
81 |             self.fail(str(e))
82 | 
83 | 
84 | 


--------------------------------------------------------------------------------
/tests/test_stdin_socket.py:
--------------------------------------------------------------------------------
 1 | import sys
 2 | import tornado
 3 | import time
 4 | import socket
 5 | 
 6 | from tests.support import TestCircus, TimeoutException
 7 | from tests.support import skipIf, IS_WINDOWS
 8 | from circus.stream import QueueStream, Empty
 9 | from circus.util import tornado_sleep
10 | from circus.sockets import CircusSocket
11 | 
12 | 
13 | def run_process(test_file):
14 |     # get stdin socket and output bound address
15 |     sock = socket.fromfd(0, socket.AF_INET, socket.SOCK_STREAM)
16 |     hostaddr, port = sock.getsockname()
17 |     sys.stdout.write("%s %s" % (hostaddr, port))
18 |     return 1
19 | 
20 | 
21 | @tornado.gen.coroutine
22 | def read_from_stream(stream, timeout=5):
23 |     start = time.time()
24 |     while time.time() - start < timeout:
25 |         try:
26 |             data = stream.get_nowait()
27 |             raise tornado.gen.Return(data['data'].decode('utf-8'))
28 |         except Empty:
29 |             yield tornado_sleep(.1)
30 |     raise TimeoutException('Timeout reading queue')
31 | 
32 | 
33 | class StdinSocketTest(TestCircus):
34 | 
35 |     def setUp(self):
36 |         super(StdinSocketTest, self).setUp()
37 | 
38 |     def tearDown(self):
39 |         super(StdinSocketTest, self).tearDown()
40 | 
41 |     @skipIf(IS_WINDOWS, "Stdin socket not supported")
42 |     @tornado.testing.gen_test
43 |     def test_stdin_socket(self):
44 |         cmd = 'tests.test_stdin_socket.run_process'
45 |         stream = QueueStream()
46 |         stdout_stream = {'stream': stream}
47 |         sk = CircusSocket(name='test', host='localhost', port=0)
48 |         yield self.start_arbiter(cmd=cmd, stdout_stream=stdout_stream,
49 |                                  arbiter_kw={'sockets': [sk]},
50 |                                  stdin_socket='test', use_sockets=True)
51 | 
52 |         # check same socket in child fd 0
53 |         addr_string_actual = yield read_from_stream(stream)
54 |         addr_string_expected = "%s %s" % (sk.host, sk.port)
55 |         self.assertEqual(addr_string_actual, addr_string_expected)
56 | 
57 |         yield self.stop_arbiter()
58 | 
59 |     @skipIf(IS_WINDOWS, "Stdin socket not supported")
60 |     @tornado.testing.gen_test
61 |     def test_stdin_socket_missing_raises(self):
62 |         raised = False
63 |         try:
64 |             # expecting exception for no such socket
65 |             yield self.start_arbiter(stdin_socket='test')
66 |         except Exception:
67 |             raised = True
68 |         self.assertTrue(raised)
69 | 
70 |         yield self.stop_arbiter()
71 | 


--------------------------------------------------------------------------------
/tests/test_plugin_watchdog.py:
--------------------------------------------------------------------------------
 1 | import socket
 2 | import time
 3 | import os
 4 | import warnings
 5 | 
 6 | from tornado.testing import gen_test
 7 | 
 8 | from tests.support import TestCircus, Process, async_poll_for
 9 | from tests.support import async_run_plugin as arp
10 | from circus.plugins.watchdog import WatchDog
11 | 
12 | 
13 | class DummyWatchDogged(Process):
14 |     def run(self):
15 |         self._write('STARTWD')
16 |         sock = socket.socket(socket.AF_INET,
17 |                              socket.SOCK_DGRAM)  # UDP
18 |         try:
19 |             my_pid = os.getpid()
20 |             for _ in range(5):
21 |                 message = "{pid};{time}".format(pid=my_pid, time=time.time())
22 |                 sock.sendto(message.encode('utf-8'), ('127.0.0.1', 1664))
23 |                 time.sleep(0.5)
24 |         finally:
25 |             sock.close()
26 | 
27 | 
28 | def run_dummy_watchdogged(test_file):
29 |     process = DummyWatchDogged(test_file)
30 |     process.run()
31 |     return 1
32 | 
33 | 
34 | def get_pid_status(queue, plugin):
35 |     queue.put(plugin.pid_status)
36 | 
37 | 
38 | fqn = 'tests.test_plugin_watchdog.run_dummy_watchdogged'
39 | 
40 | 
41 | class TestPluginWatchDog(TestCircus):
42 | 
43 |     @gen_test
44 |     def test_watchdog_discovery_found(self):
45 |         yield self.start_arbiter(fqn)
46 |         yield async_poll_for(self.test_file, 'STARTWD')
47 |         pubsub = self.arbiter.pubsub_endpoint
48 | 
49 |         config = {'loop_rate': 0.1, 'watchers_regex': "^test.*$"}
50 |         with warnings.catch_warnings():
51 |             pid_status = yield arp(WatchDog, config,
52 |                                    get_pid_status,
53 |                                    endpoint=self.arbiter.endpoint,
54 |                                    pubsub_endpoint=pubsub)
55 |         self.assertEqual(len(pid_status), 1, pid_status)
56 |         yield self.stop_arbiter()
57 | 
58 |     @gen_test
59 |     def test_watchdog_discovery_not_found(self):
60 |         yield self.start_arbiter(fqn)
61 |         yield async_poll_for(self.test_file, 'START')
62 |         pubsub = self.arbiter.pubsub_endpoint
63 | 
64 |         config = {'loop_rate': 0.1, 'watchers_regex': "^foo.*$"}
65 |         with warnings.catch_warnings():
66 |             pid_status = yield arp(WatchDog, config,
67 |                                    get_pid_status,
68 |                                    endpoint=self.arbiter.endpoint,
69 |                                    pubsub_endpoint=pubsub)
70 |         self.assertEqual(len(pid_status), 0, pid_status)
71 |         yield self.stop_arbiter()
72 | 
73 | 
74 | 


--------------------------------------------------------------------------------
/circus/commands/incrproc.py:
--------------------------------------------------------------------------------
 1 | from circus.commands.base import Command
 2 | from circus.exc import ArgumentError
 3 | from circus.util import TransformableFuture
 4 | 
 5 | 
 6 | class IncrProc(Command):
 7 |     """\
 8 |         Increment the number of processes in a watcher
 9 |         ==============================================
10 | 
11 |         This comment increment the number of processes in a watcher
12 |         by , 1 being the default
13 | 
14 |         ZMQ Message
15 |         -----------
16 | 
17 |         ::
18 | 
19 |             {
20 |                 "command": "incr",
21 |                 "properties": {
22 |                     "name": "",
23 |                     "nb": ,
24 |                     "waiting": False
25 |                 }
26 |             }
27 | 
28 |         The response return the number of processes in the 'numprocesses`
29 |         property::
30 | 
31 |             { "status": "ok", "numprocesses": , "time", "timestamp" }
32 | 
33 |         Command line
34 |         ------------
35 | 
36 |         ::
37 | 
38 |             $ circusctl incr  [] [--waiting]
39 | 
40 |         Options
41 |         +++++++
42 | 
43 |         - : name of the watcher.
44 |         - : the number of processes to add.
45 | 
46 |     """
47 | 
48 |     name = "incr"
49 |     properties = ['name']
50 |     options = Command.waiting_options
51 | 
52 |     def message(self, *args, **opts):
53 |         if len(args) < 1:
54 |             raise ArgumentError("Invalid number of arguments")
55 |         options = {'name': args[0]}
56 |         if len(args) > 1:
57 |             options['nb'] = int(args[1])
58 |         options.update(opts)
59 |         return self.make_message(**options)
60 | 
61 |     def execute(self, arbiter, props):
62 |         watcher = self._get_watcher(arbiter, props.get('name'))
63 |         if watcher.singleton:
64 |             return {"numprocesses": watcher.numprocesses, "singleton": True}
65 |         else:
66 |             nb = props.get("nb", 1)
67 |             resp = TransformableFuture()
68 |             resp.set_upstream_future(watcher.incr(nb))
69 |             resp.set_transform_function(lambda x: {"numprocesses": x})
70 |             return resp
71 | 
72 |     def console_msg(self, msg):
73 |         if msg.get("status") == "ok":
74 |             if "singleton" in msg:
75 |                 return ('This watcher is a Singleton - not changing the number'
76 |                         ' of processes')
77 |             else:
78 |                 return str(msg.get("numprocesses", "ok"))
79 |         return self.console_error(msg)
80 | 


--------------------------------------------------------------------------------
/circus/commands/get.py:
--------------------------------------------------------------------------------
 1 | from circus.commands.base import Command
 2 | from circus.exc import ArgumentError, MessageError
 3 | from circus.util import convert_opt
 4 | 
 5 | 
 6 | class Get(Command):
 7 |     """\
 8 |         Get the value of specific watcher options
 9 |         =========================================
10 | 
11 |         This command can be used to query the current value of one or
12 |         more watcher options.
13 | 
14 |         ZMQ Message
15 |         -----------
16 | 
17 |         ::
18 | 
19 |             {
20 |                 "command": "get",
21 |                 "properties": {
22 |                     "keys": ["key1, "key2"]
23 |                     "name": "nameofwatcher"
24 |                 }
25 |             }
26 | 
27 |         A request message contains two properties:
28 | 
29 |         - keys: list, The option keys for which you want to get the values
30 |         - name: name of watcher
31 | 
32 |         The response object has a property ``options`` which is a
33 |         dictionary of option names and values.
34 | 
35 |         eg::
36 | 
37 |             {
38 |                 "status": "ok",
39 |                 "options": {
40 |                     "graceful_timeout": 300,
41 |                     "send_hup": True,
42 |                 },
43 |                 time': 1332202594.754644
44 |             }
45 | 
46 | 
47 |         Command line
48 |         ------------
49 | 
50 |         ::
51 | 
52 |             $ circusctl get   
53 | 
54 |     """
55 | 
56 |     name = "get"
57 |     properties = ['name', 'keys']
58 | 
59 |     def message(self, *args, **opts):
60 |         if len(args) < 2:
61 |             raise ArgumentError("Invalid number of arguments")
62 | 
63 |         return self.make_message(name=args[0], keys=args[1:])
64 | 
65 |     def execute(self, arbiter, props):
66 |         watcher = self._get_watcher(arbiter, props.get('name'))
67 | 
68 |         # get options values. It return an error if one of the asked
69 |         # options isn't found
70 |         options = {}
71 |         for name in props.get('keys', []):
72 |             if name in watcher.optnames:
73 |                 options[name] = getattr(watcher, name)
74 |             else:
75 |                 raise MessageError("%r option not found" % name)
76 | 
77 |         return {"options": options}
78 | 
79 |     def console_msg(self, msg):
80 |         if msg['status'] == "ok":
81 |             ret = []
82 |             for k, v in msg.get('options', {}).items():
83 |                 ret.append("%s: %s" % (k, convert_opt(k, v)))
84 |             return "\n".join(ret)
85 |         return self.console_error(msg)
86 | 


--------------------------------------------------------------------------------
/docs/source/for-devs/library.rst:
--------------------------------------------------------------------------------
 1 | .. _library:
 2 | 
 3 | Circus Library
 4 | ##############
 5 | 
 6 | The Circus package is composed of a high-level :func:`get_arbiter`
 7 | function and many classes. In most cases, using the high-level function
 8 | should be enough, as it creates everything that is needed for Circus to
 9 | run.
10 | 
11 | You can subclass Circus' classes if you need more granularity than what is
12 | offered by the configuration.
13 | 
14 | 
15 | The get_arbiter function
16 | ========================
17 | 
18 | :func:`get_arbiter` is just a convenience on top of the various
19 | circus classes. It creates an :term:`arbiter` (class :class:`Arbiter`) instance
20 | with the provided options, which in turn runs a single :class:`Watcher` with a
21 | single :class:`Process`.
22 | 
23 | 
24 | .. autofunction:: circus.get_arbiter
25 | 
26 | Example:
27 | 
28 | .. code-block:: python
29 | 
30 |    from circus import get_arbiter
31 | 
32 |    arbiter = get_arbiter([{"cmd": "myprogram", "numprocesses": 3}])
33 |    try:
34 |        arbiter.start()
35 |    finally:
36 |        arbiter.stop()
37 | 
38 | 
39 | Classes
40 | =======
41 | 
42 | Circus provides a series of classes you can use to implement your own process
43 | manager:
44 | 
45 | - :class:`Process`: wraps a running process and provides a few helpers on top
46 |   of it.
47 | 
48 | - :class:`Watcher`: run several instances of :class:`Process` against the same
49 |   command. Manage the death and life of processes.
50 | 
51 | - :class:`Arbiter`: manages several :class:`Watcher` instances.
52 | 
53 | 
54 | .. autoclass:: circus.process.Process
55 |    :members: pid, stdout, stderr, send_signal, stop, age, info,
56 |              children, is_child, send_signal_child, send_signal_children,
57 |              status
58 | 
59 | 
60 | Example::
61 | 
62 |     >>> from circus.process import Process
63 |     >>> process = Process('Top', 'top', shell=True)
64 |     >>> process.age()
65 |     3.0107998847961426
66 |     >>> process.info()
67 |     'Top: 6812  N/A tarek Zombie N/A N/A N/A N/A N/A'
68 |     >>> process.status
69 |     1
70 |     >>> process.stop()
71 |     >>> process.status
72 |     2
73 |     >>> process.info()
74 |     'No such process (stopped?)'
75 | 
76 | 
77 | .. autoclass:: circus.watcher.Watcher
78 |    :members: notify_event, reap_processes, manage_processes, reap_and_manage_processes,
79 |              spawn_processes, spawn_process, kill_process,kill_processes, send_signal_child, stop,start,
80 |              restart, reload, do_action
81 | 
82 | 
83 | .. autoclass:: circus.arbiter.Arbiter
84 |    :members: start, stop, reload, numprocesses, numwatchers, get_watcher, add_watcher
85 | 


--------------------------------------------------------------------------------
/circus/stats/__init__.py:
--------------------------------------------------------------------------------
 1 | 
 2 | """
 3 | Stats architecture:
 4 | 
 5 |  * streamer.StatsStreamer listens to circusd events and maintain a list of pids
 6 |  * collector.StatsCollector runs a pool of threads that compute stats for each
 7 |    pid in the list. Each stat is pushed in a queue
 8 |  * publisher.StatsPublisher continuously pushes those stats in a zmq PUB socket
 9 |  * client.StatsClient is a simple subscriber that can be used to intercept the
10 |    stream of stats.
11 | """
12 | import sys
13 | import signal
14 | import argparse
15 | 
16 | from circus.stats.streamer import StatsStreamer
17 | from circus.util import configure_logger
18 | from circus.sighandler import SysHandler
19 | from circus import logger
20 | from circus import util
21 | from circus import __version__
22 | 
23 | 
24 | def main():
25 |     desc = 'Runs the stats aggregator for Circus'
26 |     parser = argparse.ArgumentParser(description=desc)
27 | 
28 |     parser.add_argument('--endpoint',
29 |                         help='The circusd ZeroMQ socket to connect to',
30 |                         default=util.DEFAULT_ENDPOINT_DEALER)
31 | 
32 |     parser.add_argument('--pubsub',
33 |                         help='The circusd ZeroMQ pub/sub socket to connect to',
34 |                         default=util.DEFAULT_ENDPOINT_SUB)
35 | 
36 |     parser.add_argument('--statspoint',
37 |                         help='The ZeroMQ pub/sub socket to send data to',
38 |                         default=util.DEFAULT_ENDPOINT_STATS)
39 | 
40 |     parser.add_argument('--log-level', dest='loglevel', default='info',
41 |                         help="log level")
42 | 
43 |     parser.add_argument('--log-output', dest='logoutput', default='-',
44 |                         help="log output")
45 | 
46 |     parser.add_argument('--version', action='store_true',
47 |                         default=False,
48 |                         help='Displays Circus version and exits.')
49 | 
50 |     parser.add_argument('--ssh', default=None, help='SSH Server')
51 | 
52 |     args = parser.parse_args()
53 | 
54 |     if args.version:
55 |         print(__version__)
56 |         sys.exit(0)
57 | 
58 |     # configure the logger
59 |     configure_logger(logger, args.loglevel, args.logoutput)
60 | 
61 |     stats = StatsStreamer(args.endpoint, args.pubsub, args.statspoint,
62 |                           args.ssh)
63 | 
64 |     # Register some sighandlers to stop the loop when killed
65 |     for sig in SysHandler.SIGNALS:
66 |         signal.signal(
67 |             sig, lambda *_: stats.loop.add_callback_from_signal(stats.stop)
68 |         )
69 | 
70 |     try:
71 |         stats.start()
72 |     finally:
73 |         stats.stop()
74 |         sys.exit(0)
75 | 
76 | 
77 | if __name__ == '__main__':
78 |     main()
79 | 


--------------------------------------------------------------------------------
/tests/test_plugin_flapping.py:
--------------------------------------------------------------------------------
 1 | from unittest.mock import patch
 2 | 
 3 | from tests.support import TestCircus
 4 | from circus.plugins.flapping import Flapping
 5 | 
 6 | 
 7 | class TestFlapping(TestCircus):
 8 | 
 9 |     def _flapping_plugin(self, **config):
10 |         plugin = self.make_plugin(Flapping, active=True, **config)
11 |         plugin.configs['test'] = {'active': True}
12 |         plugin.timelines['test'] = [1, 2]
13 |         return plugin
14 | 
15 |     def test_default_config(self):
16 |         plugin = self._flapping_plugin()
17 |         self.assertEqual(plugin.attempts, 2)
18 |         self.assertEqual(plugin.window, 1)
19 |         self.assertEqual(plugin.retry_in, 7)
20 |         self.assertEqual(plugin.max_retry, 5)
21 | 
22 |     @patch.object(Flapping, 'check')
23 |     def test_reap_message_calls_check(self, check_mock):
24 |         plugin = self._flapping_plugin()
25 |         topic = 'watcher.test.reap'
26 | 
27 |         plugin.handle_recv([topic, None])
28 | 
29 |         check_mock.assert_called_with('test')
30 | 
31 |     @patch.object(Flapping, 'cast')
32 |     @patch('circus.plugins.flapping.Timer')
33 |     def test_below_max_retry_triggers_restart(self, timer_mock, cast_mock):
34 |         plugin = self._flapping_plugin(max_retry=5)
35 |         plugin.tries['test'] = 4
36 | 
37 |         plugin.check('test')
38 | 
39 |         cast_mock.assert_called_with("stop", name="test")
40 |         self.assertTrue(timer_mock.called)
41 | 
42 |     @patch.object(Flapping, 'cast')
43 |     @patch('circus.plugins.flapping.Timer')
44 |     def test_above_max_retry_triggers_final_stop(self, timer_mock, cast_mock):
45 |         plugin = self._flapping_plugin(max_retry=5)
46 |         plugin.tries['test'] = 5
47 | 
48 |         plugin.check('test')
49 | 
50 |         cast_mock.assert_called_with("stop", name="test")
51 |         self.assertFalse(timer_mock.called)
52 | 
53 |     def test_beyond_window_resets_tries(self):
54 |         plugin = self._flapping_plugin(max_retry=-1)
55 |         plugin.tries['test'] = 1
56 |         timestamp_beyond_window = plugin.window + plugin.check_delay + 1
57 |         plugin.timelines['test'] = [0, timestamp_beyond_window]
58 | 
59 |         plugin.check('test')
60 | 
61 |         self.assertEqual(plugin.tries['test'], 0)
62 | 
63 |     @patch.object(Flapping, 'cast')
64 |     @patch('circus.plugins.flapping.Timer')
65 |     def test_minus_one_max_retry_triggers_restart(self, timer_mock, cast_mock):
66 |         plugin = self._flapping_plugin(max_retry=-1)
67 |         plugin.timelines['test'] = [1, 2]
68 |         plugin.tries['test'] = 5
69 | 
70 |         plugin.check('test')
71 | 
72 |         cast_mock.assert_called_with("stop", name="test")
73 |         self.assertTrue(timer_mock.called)
74 | 
75 | 
76 | 


--------------------------------------------------------------------------------
/tests/test_validate_option.py:
--------------------------------------------------------------------------------
 1 | from tests.support import TestCase, IS_WINDOWS
 2 | from unittest.mock import patch
 3 | 
 4 | from circus.commands.util import validate_option
 5 | from circus.exc import MessageError
 6 | 
 7 | 
 8 | class TestValidateOption(TestCase):
 9 | 
10 |     def test_uidgid(self):
11 |         self.assertRaises(MessageError, validate_option, 'uid', {})
12 |         validate_option('uid', 1)
13 |         validate_option('uid', 'user')
14 |         self.assertRaises(MessageError, validate_option, 'gid', {})
15 |         validate_option('gid', 1)
16 |         validate_option('gid', 'user')
17 | 
18 |     @patch('warnings.warn')
19 |     def test_stdout_stream(self, warn):
20 |         key = 'stdout_stream'
21 |         self.assertRaises(
22 |             MessageError, validate_option, key, 'something')
23 |         self.assertRaises(MessageError, validate_option, key, {})
24 |         validate_option(key, {'class': 'MyClass'})
25 |         validate_option(
26 |             key, {'class': 'MyClass', 'my_option': '1'})
27 |         validate_option(
28 |             key, {'class': 'MyClass', 'refresh_time': 1})
29 | 
30 |         msg = "'refresh_time' is deprecated and not useful anymore for %r" % key
31 |         warn.assert_any_call(msg)
32 | 
33 |     @patch('warnings.warn')
34 |     def test_stderr_stream(self, warn):
35 |         key = 'stderr_stream'
36 |         self.assertRaises(
37 |             MessageError, validate_option, key, 'something')
38 |         self.assertRaises(MessageError, validate_option, key, {})
39 | 
40 |         validate_option(key, {'class': 'MyClass'})
41 |         validate_option(
42 |             key, {'class': 'MyClass', 'my_option': '1'})
43 |         validate_option(
44 |             key, {'class': 'MyClass', 'refresh_time': 1})
45 | 
46 |         msg = "'refresh_time' is deprecated and not useful anymore for %r" % key
47 |         warn.assert_any_call(msg)
48 | 
49 |     def test_hooks(self):
50 |         validate_option('hooks', {'before_start': ['all', False]})
51 | 
52 |         # make sure we control the hook names
53 |         self.assertRaises(MessageError, validate_option, 'hooks',
54 |                           {'IDONTEXIST': ['all', False]})
55 | 
56 |     def test_rlimit(self):
57 |         if IS_WINDOWS:
58 |             # rlimits are not supported on Windows
59 |             self.assertRaises(MessageError, validate_option, 'rlimit_core', 1)
60 |         else:
61 |             validate_option('rlimit_core', 1)
62 | 
63 |             # require int parameter
64 |             self.assertRaises(MessageError, validate_option,
65 |                               'rlimit_core', '1')
66 | 
67 |             # require valid rlimit settings
68 |             self.assertRaises(MessageError, validate_option, 'rlimit_foo', 1)
69 | 
70 | 
71 | 


--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
  1 | [build-system]
  2 | requires = ['flit_core>=3.4,<4']
  3 | build-backend = 'flit_core.buildapi'
  4 | 
  5 | [project]
  6 | name = 'circus'
  7 | dynamic = ['description', 'version']
  8 | authors = [
  9 |     {name = 'Mozilla Foundation & contributors', email = 'services-dev@lists.mozila.org'}
 10 | ]
 11 | readme = 'README.md'
 12 | license = {file = 'LICENSE'}
 13 | classifiers = [
 14 |     'Development Status :: 5 - Production/Stable',
 15 |     'License :: OSI Approved :: Apache Software License',
 16 |     'Operating System :: POSIX :: Linux',
 17 |     'Operating System :: MacOS :: MacOS X',
 18 |     'Operating System :: Microsoft :: Windows',
 19 |     'Programming Language :: Python',
 20 |     'Programming Language :: Python :: 3.7',
 21 |     'Programming Language :: Python :: 3.8',
 22 |     'Programming Language :: Python :: 3.9',
 23 |     'Programming Language :: Python :: 3.10',
 24 |     'Programming Language :: Python :: 3.11',
 25 |     'Programming Language :: Python :: 3.12',
 26 |     'Programming Language :: Python :: 3.13',
 27 | ]
 28 | requires-python = '>=3.7'
 29 | dependencies = [
 30 |     'psutil',
 31 |     'pyzmq>=17.0',
 32 |     'tornado>=5.0.2',
 33 | ]
 34 | 
 35 | [project.urls]
 36 | Source = 'https://github.com/circus-tent/circus'
 37 | Documentation = 'https://circus.readthedocs.io'
 38 | 
 39 | [project.optional-dependencies]
 40 | test = [
 41 |     'coverage',
 42 |     'flake8==2.1.0',
 43 |     'gevent',
 44 |     'mock',
 45 |     'pytest',
 46 |     'pytest-cov',
 47 |     'pyyaml',
 48 |     'tox',
 49 | ]
 50 | 
 51 | [project.scripts]
 52 | circusd = 'circus.circusd:main'
 53 | circusd-stats = 'circus.stats:main'
 54 | circusctl = 'circus.circusctl:main'
 55 | circus-top = 'circus.stats.client:main'
 56 | circus-plugin = 'circus.plugins:main'
 57 | 
 58 | [tool.flit.module]
 59 | name = 'circus'
 60 | 
 61 | [tool.flit.sdist]
 62 | exclude = [
 63 |     '.github/',
 64 |     'docs/',
 65 |     'examples/',
 66 |     'tests/',
 67 |     '.gitignore',
 68 | ]
 69 | 
 70 | [tool.pytest.ini_options]
 71 | minversion = '6.0'
 72 | testpaths = [
 73 |     'tests',
 74 | ]
 75 | 
 76 | [tool.tox]
 77 | legacy_tox_ini = """
 78 | [tox]
 79 | isolated_build = True
 80 | envlist = py37,py38,py39,py310,py311,pypy37,pypy38,pypy39,flake8,docs
 81 | 
 82 | [testenv]
 83 | passenv = PWD
 84 | deps =
 85 |     coverage
 86 |     gevent
 87 |     mock
 88 |     pytest==7.4.4
 89 |     pytest-cov
 90 |     pyyaml
 91 |     pyzmq>=17.0
 92 |     tornado==6.2
 93 | 
 94 | setenv =
 95 |     TESTING=1
 96 |     PYTHONUNBUFFERED=1
 97 | 
 98 | commands =
 99 |     pytest --verbose -s
100 | 
101 | 
102 | [testenv:docs]
103 | whitelist_externals = make
104 | deps =
105 |     sphinx
106 |     mozilla-sphinx-theme
107 | commands = make -C docs html
108 | 
109 | 
110 | [testenv:flake8]
111 | deps = flake8==2.1.0
112 | commands = flake8 circus
113 | """
114 | 


--------------------------------------------------------------------------------
/circus/commands/globaloptions.py:
--------------------------------------------------------------------------------
  1 | from circus.commands.base import Command
  2 | from circus.exc import MessageError
  3 | from circus.util import convert_opt
  4 | 
  5 | 
  6 | _OPTIONS = ('endpoint', 'stats_endpoint', 'pubsub_endpoint',
  7 |             'check_delay', 'multicast_endpoint')
  8 | 
  9 | 
 10 | class GlobalOptions(Command):
 11 |     """\
 12 |         Get the arbiter options
 13 |         =======================
 14 | 
 15 |         This command return the arbiter options
 16 | 
 17 |         ZMQ Message
 18 |         -----------
 19 | 
 20 |         ::
 21 | 
 22 |             {
 23 |                 "command": "globaloptions",
 24 |                 "properties": {
 25 |                     "key1": "val1",
 26 |                     ..
 27 |                 }
 28 |             }
 29 | 
 30 |         A message contains 2 properties:
 31 | 
 32 |         - keys: list, The option keys for which you want to get the values
 33 | 
 34 |         The response return an object with a property "options"
 35 |         containing the list of key/value returned by circus.
 36 | 
 37 |         eg::
 38 | 
 39 |             {
 40 |                 "status": "ok",
 41 |                 "options": {
 42 |                     "check_delay": 1,
 43 |                     ...
 44 |                 },
 45 |                 time': 1332202594.754644
 46 |             }
 47 | 
 48 | 
 49 | 
 50 |         Command line
 51 |         ------------
 52 | 
 53 |         ::
 54 | 
 55 |             $ circusctl globaloptions
 56 | 
 57 | 
 58 |         Options
 59 |         -------
 60 | 
 61 |         Options Keys are:
 62 | 
 63 |         - endpoint: the controller ZMQ endpoint
 64 |         - pubsub_endpoint: the pubsub endpoint
 65 |         - check_delay: the delay between two controller points
 66 |         - multicast_endpoint: the multicast endpoint for circusd cluster
 67 |           auto-discovery
 68 |     """
 69 | 
 70 |     name = "globaloptions"
 71 |     properties = []
 72 | 
 73 |     def message(self, *args, **opts):
 74 |         if len(args) > 0:
 75 |             return self.make_message(option=args[0])
 76 |         else:
 77 |             return self.make_message()
 78 | 
 79 |     def execute(self, arbiter, props):
 80 |         wanted = props.get('option')
 81 |         if wanted:
 82 |             if wanted not in _OPTIONS:
 83 |                 raise MessageError('%r not an existing option' % wanted)
 84 |             options = (wanted,)
 85 |         else:
 86 |             options = _OPTIONS
 87 | 
 88 |         res = {}
 89 | 
 90 |         for option in options:
 91 |             res[option] = getattr(arbiter, option)
 92 | 
 93 |         return {"options": res}
 94 | 
 95 |     def console_msg(self, msg):
 96 |         if msg['status'] == "ok":
 97 |             ret = []
 98 |             for k, v in msg.get('options', {}).items():
 99 |                 ret.append("%s: %s" % (k, convert_opt(k, v)))
100 |             return "\n".join(ret)
101 |         return msg['reason']
102 | 


--------------------------------------------------------------------------------
/circus/commands/set.py:
--------------------------------------------------------------------------------
 1 | from circus.commands.base import Command
 2 | from circus.commands.util import convert_option, validate_option
 3 | from circus.exc import ArgumentError, MessageError
 4 | 
 5 | 
 6 | class Set(Command):
 7 |     """\
 8 |         Set a watcher option
 9 |         ====================
10 | 
11 |         ZMQ Message
12 |         -----------
13 | 
14 |         ::
15 | 
16 |             {
17 |                 "command": "set",
18 |                 "properties": {
19 |                     "name": "nameofwatcher",
20 |                     "options": {
21 |                         "key1": "val1",
22 |                         ..
23 |                     }
24 |                     "waiting": False
25 |                 }
26 |             }
27 | 
28 | 
29 |         The response return the status "ok". See the command Options for
30 |         a list of key to set.
31 | 
32 |         Command line
33 |         ------------
34 | 
35 |         ::
36 | 
37 |             $ circusctl set      --waiting
38 | 
39 | 
40 |     """
41 | 
42 |     name = "set"
43 |     properties = ['name', 'options']
44 |     options = Command.waiting_options
45 | 
46 |     def message(self, *args, **opts):
47 |         if len(args) < 3:
48 |             raise ArgumentError("Invalid number of arguments")
49 | 
50 |         args = list(args)
51 |         watcher_name = args.pop(0)
52 |         if len(args) % 2 != 0:
53 |             raise ArgumentError("List of key/values is invalid")
54 | 
55 |         options = {}
56 |         while len(args) > 0:
57 |             kv, args = args[:2], args[2:]
58 |             kvl = kv[0].lower()
59 |             options[kvl] = convert_option(kvl, kv[1])
60 | 
61 |         if opts.get('waiting', False):
62 |             return self.make_message(name=watcher_name, waiting=True,
63 |                                      options=options)
64 |         else:
65 |             return self.make_message(name=watcher_name, options=options)
66 | 
67 |     def execute(self, arbiter, props):
68 |         watcher = self._get_watcher(arbiter, props.pop('name'))
69 |         action = 0
70 |         for key, val in props.get('options', {}).items():
71 |             if key == 'hooks':
72 |                 new_action = 0
73 |                 for name, _val in val.items():
74 |                     action = watcher.set_opt('hooks.%s' % name, _val)
75 |                     if action == 1:
76 |                         new_action = 1
77 |             else:
78 |                 new_action = watcher.set_opt(key, val)
79 | 
80 |             if new_action == 1:
81 |                 action = 1
82 |         # trigger needed action
83 |         return watcher.do_action(action)
84 | 
85 |     def validate(self, props):
86 |         super(Set, self).validate(props)
87 | 
88 |         options = props['options']
89 |         if not isinstance(options, dict):
90 |             raise MessageError("'options' property should be an object")
91 | 
92 |         for key, val in options.items():
93 |             validate_option(key, val)
94 | 


--------------------------------------------------------------------------------
/circus/pidfile.py:
--------------------------------------------------------------------------------
 1 | import errno
 2 | import os
 3 | import stat
 4 | import tempfile
 5 | 
 6 | 
 7 | class Pidfile(object):
 8 |     """
 9 |     Manage a PID file. If a specific name is provided
10 |     it and '"%s.oldpid" % name' will be used. Otherwise
11 |     we create a temp file using tempfile.mkstemp.
12 |     """
13 | 
14 |     def __init__(self, fname):
15 |         self.fname = fname
16 |         self.pid = os.getpid()
17 |         # set permissions to -rw-r--r--
18 |         self.perm_mode = (stat.S_IRUSR | stat.S_IWUSR |
19 |                           stat.S_IRGRP |
20 |                           stat.S_IROTH)
21 | 
22 |     def create(self, pid):
23 |         pid = int(pid)
24 |         oldpid = self.validate()
25 |         if oldpid:
26 |             if oldpid == pid:
27 |                 return
28 |             raise RuntimeError("pid file '{0}' is stale, current pid {1}"
29 |                                .format(self.fname, pid))
30 | 
31 |         self.pid = pid
32 | 
33 |         # Write pidfile
34 |         if self.fname:
35 |             fdir = os.path.dirname(self.fname)
36 |             if fdir and not os.path.isdir(fdir):
37 |                 raise RuntimeError("{0} doesn't exist. Can't create pidfile"
38 |                                    .format(fdir))
39 |             flags = os.O_CREAT | os.O_WRONLY | os.O_TRUNC
40 |             fd = os.open(self.fname, flags, self.perm_mode)
41 |         else:
42 |             fd, self.fname = tempfile.mkstemp()
43 | 
44 |         os.chmod(self.fname, self.perm_mode)
45 |         os.write(fd, "{0}\n".format(self.pid).encode('utf-8'))
46 |         os.fsync(fd)
47 |         os.close(fd)
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 |                 try:
59 |                     pid1 = int(f.read() or 0)
60 |                 except ValueError:
61 |                     pid1 = self.pid
62 | 
63 |             if pid1 == self.pid:
64 |                 os.unlink(self.fname)
65 |         except:  # noqa: E722
66 |             pass
67 | 
68 |     def validate(self):
69 |         """ Validate pidfile and make it stale if needed"""
70 |         if not self.fname:
71 |             return
72 |         try:
73 |             with open(self.fname, "r") as f:
74 |                 try:
75 |                     wpid = int(f.read() or 0)
76 |                 except ValueError:
77 |                     return
78 | 
79 |                 if wpid <= 0:
80 |                     return
81 | 
82 |                 try:
83 |                     os.kill(wpid, 0)
84 |                     return wpid
85 |                 except OSError as e:
86 |                     if e.args[0] == errno.ESRCH:
87 |                         return
88 |                     raise
89 |         except ValueError:
90 |             return
91 |         except IOError as e:
92 |             if e.args[0] == errno.ENOENT:
93 |                 return
94 |             raise
95 | 


--------------------------------------------------------------------------------
/circus/sighandler.py:
--------------------------------------------------------------------------------
 1 | import signal
 2 | import traceback
 3 | import sys
 4 | 
 5 | from circus import logger
 6 | from circus.client import make_json
 7 | from circus.util import IS_WINDOWS
 8 | 
 9 | 
10 | class SysHandler(object):
11 | 
12 |     _SIGNALS_NAMES = ("ILL ABRT BREAK INT TERM" if IS_WINDOWS else
13 |                       "HUP QUIT INT TERM WINCH")
14 | 
15 |     SIGNALS = [getattr(signal, "SIG%s" % x) for x in _SIGNALS_NAMES.split()]
16 | 
17 |     SIG_NAMES = dict(
18 |         (getattr(signal, name), name[3:].lower()) for name in dir(signal)
19 |         if name[:3] == "SIG" and name[3] != "_"
20 |     )
21 | 
22 |     def __init__(self, controller):
23 |         self.controller = controller
24 | 
25 |         # init signals
26 |         logger.info('Registering signals...')
27 |         self._old = {}
28 |         self._register()
29 | 
30 |     def stop(self):
31 |         for sig, callback in self._old.items():
32 |             try:
33 |                 signal.signal(sig, callback)
34 |             except ValueError:
35 |                 pass
36 | 
37 |     def _register(self):
38 |         for sig in self.SIGNALS:
39 |             self._old[sig] = signal.getsignal(sig)
40 |             signal.signal(sig, self.signal)
41 | 
42 |         # Don't let SIGQUIT and SIGUSR1 disturb active requests
43 |         # by interrupting system calls
44 |         if hasattr(signal, 'siginterrupt'):  # python >= 2.6
45 |             signal.siginterrupt(signal.SIGQUIT, False)
46 |             signal.siginterrupt(signal.SIGUSR1, False)
47 | 
48 |     def signal(self, sig, frame=None):
49 |         signame = self.SIG_NAMES.get(sig)
50 |         logger.info('Got signal SIG_%s' % signame.upper())
51 | 
52 |         if signame is not None:
53 |             try:
54 |                 handler = getattr(self, "handle_%s" % signame)
55 |                 handler()
56 |             except AttributeError:
57 |                 pass
58 |             except Exception as e:
59 |                 tb = traceback.format_exc()
60 |                 logger.error("error: %s [%s]" % (e, tb))
61 |                 sys.exit(1)
62 | 
63 |     def quit(self):
64 |         # We need to transfer the control to the loop's thread
65 |         self.controller.loop.add_callback_from_signal(
66 |             self.controller.dispatch, (None, make_json("quit"))
67 |         )
68 | 
69 |     def reload(self):
70 |         # We need to transfer the control to the loop's thread
71 |         self.controller.loop.add_callback_from_signal(
72 |             self.controller.dispatch,
73 |             (None, make_json("reload", graceful=True))
74 |         )
75 | 
76 |     def handle_int(self):
77 |         self.quit()
78 | 
79 |     def handle_term(self):
80 |         self.quit()
81 | 
82 |     def handle_quit(self):
83 |         self.quit()
84 | 
85 |     def handle_ill(self):
86 |         self.quit()
87 | 
88 |     def handle_abrt(self):
89 |         self.quit()
90 | 
91 |     def handle_break(self):
92 |         self.quit()
93 | 
94 |     def handle_winch(self):
95 |         pass
96 | 
97 |     def handle_hup(self):
98 |         self.reload()
99 | 


--------------------------------------------------------------------------------
/tests/test_stats_client.py:
--------------------------------------------------------------------------------
 1 | import time
 2 | import tempfile
 3 | import os
 4 | import sys
 5 | import tornado
 6 | 
 7 | from tests.support import TestCircus, skipIf, IS_WINDOWS
 8 | from circus.client import AsyncCircusClient
 9 | from circus.stream import FileStream
10 | from circus.util import tornado_sleep
11 | 
12 | 
13 | def run_process(*args, **kw):
14 |     try:
15 |         i = 0
16 |         while True:
17 |             sys.stdout.write('%.2f-stdout-%d-%s\n' % (time.time(),
18 |                                                       os.getpid(), i))
19 |             sys.stdout.flush()
20 |             sys.stderr.write('%.2f-stderr-%d-%s\n' % (time.time(),
21 |                                                       os.getpid(), i))
22 |             sys.stderr.flush()
23 |             time.sleep(.25)
24 |     except:  # noqa: E722
25 |         return 1
26 | 
27 | 
28 | class TestStatsClient(TestCircus):
29 | 
30 |     def setUp(self):
31 |         super(TestStatsClient, self).setUp()
32 |         self.files = []
33 | 
34 |     def _get_file(self):
35 |         fd, log = tempfile.mkstemp()
36 |         os.close(fd)
37 |         self.files.append(log)
38 |         return log
39 | 
40 |     def tearDown(self):
41 |         super(TestStatsClient, self).tearDown()
42 |         for file in self.files:
43 |             if os.path.exists(file):
44 |                 os.remove(file)
45 | 
46 |     @skipIf(IS_WINDOWS, "Streams not supported")
47 |     @tornado.testing.gen_test
48 |     def test_handler(self):
49 |         log = self._get_file()
50 |         stream = {'stream': FileStream(log)}
51 |         cmd = 'tests.test_stats_client.run_process'
52 |         stdout_stream = stream
53 |         stderr_stream = stream
54 |         yield self.start_arbiter(cmd=cmd, stdout_stream=stdout_stream,
55 |                                  stderr_stream=stderr_stream, stats=True,
56 |                                  debug=False)
57 | 
58 |         # waiting for data to appear in the file stream
59 |         empty = True
60 |         while empty:
61 |             with open(log) as f:
62 |                 empty = f.read() == ''
63 |             yield tornado_sleep(.1)
64 | 
65 |         # checking that our system is live and running
66 |         client = AsyncCircusClient(endpoint=self.arbiter.endpoint)
67 |         res = yield client.send_message('list')
68 | 
69 |         watchers = sorted(res['watchers'])
70 |         self.assertEqual(['circusd-stats', 'test'], watchers)
71 | 
72 |         # making sure the stats process run
73 |         res = yield client.send_message('status', name='test')
74 |         self.assertEqual(res['status'], 'active')
75 | 
76 |         res = yield client.send_message('status', name='circusd-stats')
77 |         self.assertEqual(res['status'], 'active')
78 | 
79 |         # playing around with the stats now: we should get some !
80 |         from circus.stats.client import StatsClient
81 |         client = StatsClient(endpoint=self.arbiter.stats_endpoint)
82 | 
83 |         message_iterator = client.iter_messages()
84 | 
85 |         for i in range(10):
86 |             watcher, pid, stat = next(message_iterator)
87 |             self.assertTrue(watcher in ('test', 'circusd-stats', 'circus'),
88 |                             watcher)
89 |         yield self.stop_arbiter()
90 | 
91 | 
92 | 


--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
  1 | Circus: A Process & Socket Manager
  2 | ##################################
  3 | 
  4 | .. image:: circus-medium.png
  5 |    :align: right
  6 | 
  7 | Circus is a Python program which can be used to monitor and control processes and sockets.
  8 | 
  9 | Circus can be driven via a command-line interface, a web interface or programmatically through
 10 | its python API.
 11 | 
 12 | To install it and try its features check out the :ref:`examples`, or read the rest of this page
 13 | for a quick introduction.
 14 | 
 15 | 
 16 | Running a Circus Daemon
 17 | -----------------------
 18 | 
 19 | 
 20 | Circus provides a command-line script call **circusd** that can be used
 21 | to manage :term:`processes` organized in one or more :term:`watchers`.
 22 | 
 23 | Circus' command-line tool is configurable using an ini-style
 24 | configuration file.
 25 | 
 26 | Here's a very minimal example:
 27 | 
 28 | .. code-block:: ini
 29 | 
 30 |     [watcher:program]
 31 |     cmd = python myprogram.py
 32 |     numprocesses = 5
 33 | 
 34 |     [watcher:anotherprogram]
 35 |     cmd = another_program
 36 |     numprocesses = 2
 37 | 
 38 | 
 39 | The file is then passed to *circusd*::
 40 | 
 41 |     $ circusd example.ini
 42 | 
 43 | 
 44 | Besides processes, Circus can also bind sockets. Since every process managed by
 45 | Circus is a child of the main Circus daemon, that means any program that's
 46 | controlled by Circus can use those sockets.
 47 | 
 48 | Running a socket is as simple as adding a *socket* section in the config file:
 49 | 
 50 | .. code-block:: ini
 51 | 
 52 |     [socket:mysocket]
 53 |     host = localhost
 54 |     port = 8080
 55 | 
 56 | To learn more about sockets, see :ref:`sockets`.
 57 | 
 58 | To understand why it's a killer feature, read :ref:`whycircussockets`.
 59 | 
 60 | 
 61 | Controlling Circus
 62 | ------------------
 63 | 
 64 | Circus provides two command-line tools to manage your running daemon:
 65 | 
 66 | - *circusctl*, a management console you can use to perform
 67 |   actions such as adding or removing :term:`workers`
 68 | 
 69 | - *circus-top*, a top-like console you can use to display the memory and
 70 |   cpu usage of your running Circus.
 71 | 
 72 | To learn more about these, see :ref:`cli`
 73 | 
 74 | Circus also offers a web dashboard that can connect to a
 75 | running Circus daemon and let you monitor and interact with it.
 76 | 
 77 | To learn more about this feature, see :ref:`circushttpd`
 78 | 
 79 | 
 80 | What now ?
 81 | ==========
 82 | 
 83 | If you are a developer and want to leverage Circus in your own project,
 84 | write plugins or hooks, go to :ref:`fordevs`.
 85 | 
 86 | If you are an ops and want to manage your processes using Circus,
 87 | go to :ref:`forops`.
 88 | 
 89 | 
 90 | Contributions and Feedback
 91 | ==========================
 92 | 
 93 | More on contributing: :ref:`contribs`.
 94 | 
 95 | 
 96 | Useful Links:
 97 | 
 98 | - There's a mailing-list for any feedback or question: http://tech.groups.yahoo.com/group/circus-dev/
 99 | - The repository and issue tracker are on GitHub : https://github.com/circus-tent/circus
100 | - Join us on the IRC : Freenode, channel **#mozilla-circus**
101 | 
102 | 
103 | Documentation index
104 | ===================
105 | 
106 | .. toctree::
107 |    :maxdepth: 2
108 | 
109 |    installation
110 |    tutorial/index
111 |    for-ops/index
112 |    for-devs/index
113 |    usecases
114 |    design/index
115 |    contributing
116 |    faq
117 |    changelog
118 |    man/index
119 |    glossary
120 |    copyright
121 | 
122 | 
123 | 


--------------------------------------------------------------------------------
/tests/test_controller.py:
--------------------------------------------------------------------------------
 1 | from tests.support import TestCase, get_ioloop
 2 | from circus.controller import Controller
 3 | from circus.util import DEFAULT_ENDPOINT_MULTICAST
 4 | from circus import logger
 5 | import circus.controller
 6 | 
 7 | from unittest import mock
 8 | 
 9 | 
10 | class TestController(TestCase):
11 | 
12 |     def test_add_job(self):
13 |         arbiter = mock.MagicMock()
14 | 
15 |         class MockedController(Controller):
16 |             called = False
17 | 
18 |             def _init_stream(self):
19 |                 pass  # NO OP
20 | 
21 |             def initialize(self):
22 |                 pass  # NO OP
23 | 
24 |             def dispatch(self, job):
25 |                 self.called = True
26 |                 self.loop.stop()
27 | 
28 |         loop = get_ioloop()
29 |         controller = MockedController('endpoint', 'multicast_endpoint',
30 |                                       mock.sentinel.context, loop, arbiter,
31 |                                       check_delay=-1.0)
32 | 
33 |         controller.dispatch((None, 'something'))
34 |         controller.start()
35 |         loop.start()
36 |         self.assertTrue(controller.called)
37 | 
38 |     def _multicast_side_effect_helper(self, side_effect):
39 |         arbiter = mock.MagicMock()
40 |         loop = mock.MagicMock()
41 |         context = mock.sentinel.context
42 | 
43 |         controller = circus.controller.Controller(
44 |             'endpoint', DEFAULT_ENDPOINT_MULTICAST, context, loop, arbiter
45 |         )
46 | 
47 |         with mock.patch('circus.util.create_udp_socket') as m:
48 |             m.side_effect = side_effect
49 |             circus.controller.create_udp_socket = m
50 | 
51 |             with mock.patch.object(logger, 'warning') as mock_logger_warn:
52 |                 controller._init_multicast_endpoint()
53 |                 self.assertTrue(mock_logger_warn.called)
54 | 
55 |     def test_multicast_ioerror(self):
56 |         self._multicast_side_effect_helper(IOError)
57 | 
58 |     def test_multicast_oserror(self):
59 |         self._multicast_side_effect_helper(OSError)
60 | 
61 |     def test_multicast_valueerror(self):
62 |         arbiter = mock.MagicMock()
63 |         loop = mock.MagicMock()
64 |         context = mock.sentinel.context
65 | 
66 |         wrong_multicast_endpoint = 'udp://127.0.0.1:12027'
67 |         controller = Controller('endpoint', wrong_multicast_endpoint,
68 |                                 context, loop, arbiter)
69 | 
70 |         with mock.patch.object(logger, 'warning') as mock_logger_warn:
71 |             controller._init_multicast_endpoint()
72 |             self.assertTrue(mock_logger_warn.called)
73 | 
74 |     def test_garbage_message(self):
75 |         class MockedController(Controller):
76 |             called = False
77 | 
78 |             def dispatch(self, job, future=None):
79 |                 self.called = True
80 | 
81 |             def send_response(self, mid, cid, msg, resp, cast=False):
82 |                 self.called = True
83 | 
84 |         arbiter = mock.MagicMock()
85 |         loop = mock.MagicMock()
86 |         context = mock.sentinel.context
87 |         controller = MockedController('endpoint', 'multicast_endpoint',
88 |                                       context, loop, arbiter)
89 |         controller.handle_message(b'hello')
90 |         self.assertFalse(controller.called)
91 |         controller.handle_message([b'383ec229eb5d47f7bdd470dd3d6734a3',
92 |                                    b'{"command":"add", "foo": "bar"}'])
93 |         self.assertTrue(controller.called)
94 | 
95 | 
96 | 


--------------------------------------------------------------------------------
/circus/commands/base.py:
--------------------------------------------------------------------------------
  1 | import copy
  2 | import textwrap
  3 | import time
  4 | 
  5 | from circus.exc import MessageError
  6 | from circus.commands import errors
  7 | 
  8 | 
  9 | KNOWN_COMMANDS = []
 10 | 
 11 | 
 12 | def get_commands():
 13 |     commands = {}
 14 |     for c in KNOWN_COMMANDS:
 15 |         cmd = c()
 16 |         commands[c.name] = cmd.copy()
 17 |     return commands
 18 | 
 19 | 
 20 | def ok(props=None):
 21 |     resp = {"status": "ok", "time": time.time()}
 22 |     if props:
 23 |         resp.update(props)
 24 |     return resp
 25 | 
 26 | 
 27 | def error(reason="unknown", tb=None, errno=errors.NOT_SPECIFIED):
 28 |     return {
 29 |         "status": "error",
 30 |         "reason": reason,
 31 |         "tb": tb,
 32 |         "time": time.time(),
 33 |         "errno": errno
 34 |     }
 35 | 
 36 | 
 37 | class CommandMeta(type):
 38 | 
 39 |     def __new__(cls, name, bases, attrs):
 40 |         super_new = type.__new__
 41 |         parents = [b for b in bases if isinstance(b, CommandMeta)]
 42 | 
 43 |         if not parents:
 44 |             return super_new(cls, name, bases, attrs)
 45 | 
 46 |         attrs["order"] = len(KNOWN_COMMANDS)
 47 |         new_class = super_new(cls, name, bases, attrs)
 48 |         new_class.fmt_desc()
 49 |         KNOWN_COMMANDS.append(new_class)
 50 |         return new_class
 51 | 
 52 |     def fmt_desc(cls):
 53 |         desc = textwrap.dedent(cls.__doc__).strip()
 54 |         setattr(cls, "desc",  desc)
 55 |         setattr(cls, "short", desc.splitlines()[0])
 56 | 
 57 | 
 58 | class Command(object):
 59 | 
 60 |     name = None
 61 |     msg_type = "dealer"
 62 |     options = []
 63 |     properties = []
 64 |     waiting = False
 65 |     waiting_options = [('waiting', 'waiting', False,
 66 |                         "Waiting the real end of the process")]
 67 | 
 68 |     ##################################################
 69 |     # These methods run within the circusctl process #
 70 |     ##################################################
 71 | 
 72 |     def make_message(self, **props):
 73 |         name = props.pop("command", self.name)
 74 |         return {"command": name, "properties": props or {}}
 75 | 
 76 |     def message(self, *args, **opts):
 77 |         raise NotImplementedError("message function isn't implemented")
 78 | 
 79 |     def console_error(self, msg):
 80 |         return "error: %s" % msg.get("reason")
 81 | 
 82 |     def console_msg(self, msg):
 83 |         if msg.get('status') == "ok":
 84 |             return "ok"
 85 |         return self.console_error(msg)
 86 | 
 87 |     def copy(self):
 88 |         return copy.copy(self)
 89 | 
 90 |     ################################################
 91 |     # These methods run within the circusd process #
 92 |     ################################################
 93 | 
 94 |     def execute(self, arbiter, props):
 95 |         raise NotImplementedError("execute function is not implemented")
 96 | 
 97 |     def _get_watcher(self, arbiter, watcher_name):
 98 |         """Get watcher from the arbiter if any."""
 99 |         try:
100 |             return arbiter.get_watcher(watcher_name.lower())
101 |         except KeyError:
102 |             raise MessageError("program %s not found" % watcher_name)
103 | 
104 |     def validate(self, props):
105 |         if not self.properties:
106 |             return
107 | 
108 |         for propname in self.properties:
109 |             if propname not in props:
110 |                 raise MessageError("message invalid %r is missing" % propname)
111 | 
112 | 
113 | Command = CommandMeta('Command', (Command,), {})
114 | 


--------------------------------------------------------------------------------
/tests/test_stats_streamer.py:
--------------------------------------------------------------------------------
  1 | import os
  2 | import tempfile
  3 | 
  4 | from unittest import mock
  5 | 
  6 | from tests.support import TestCircus
  7 | from circus.stats.streamer import StatsStreamer
  8 | from circus import client
  9 | 
 10 | 
 11 | class _StatsStreamer(StatsStreamer):
 12 | 
 13 |     msgs = []
 14 | 
 15 |     def handle_recv(self, data):
 16 |         self.msgs.append(data)
 17 | 
 18 | 
 19 | class FakeStreamer(StatsStreamer):
 20 |     def __init__(self, *args, **kwargs):
 21 |         self._initialize()
 22 | 
 23 | 
 24 | class TestStatsStreamer(TestCircus):
 25 | 
 26 |     def setUp(self):
 27 |         self.old = client.CircusClient.call
 28 |         client.CircusClient.call = self._call
 29 |         fd, self._unix = tempfile.mkstemp()
 30 |         os.close(fd)
 31 | 
 32 |     def tearDown(self):
 33 |         client.CircusClient.call = self.old
 34 |         os.remove(self._unix)
 35 | 
 36 |     def _call(self, cmd):
 37 |         what = cmd['command']
 38 |         if what == 'list':
 39 |             name = cmd['properties'].get('name')
 40 |             if name is None:
 41 |                 return {'watchers': ['one', 'two', 'three']}
 42 |             return {'pids': [123, 456]}
 43 |         elif what == 'dstats':
 44 |             return {'info': {'pid': 789}}
 45 |         elif what == 'listsockets':
 46 |             return {'status': 'ok',
 47 |                     'sockets': [{'path': self._unix,
 48 |                                  'fd': 5,
 49 |                                  'name': 'XXXX',
 50 |                                  'backlog': 2048}],
 51 |                     'time': 1369647058.967524}
 52 | 
 53 |         raise NotImplementedError(cmd)
 54 | 
 55 |     def test_get_pids_circus(self):
 56 |         streamer = FakeStreamer()
 57 |         streamer.circus_pids = {1234: 'circus-top', 1235: 'circusd'}
 58 |         self.assertEqual(streamer.get_pids('circus'), [1234, 1235])
 59 | 
 60 |     def test_get_pids(self):
 61 |         streamer = FakeStreamer()
 62 |         streamer._pids['foobar'] = [1234, 1235]
 63 |         self.assertEqual(streamer.get_pids('foobar'), [1234, 1235])
 64 | 
 65 |     def test_get_all_pids(self):
 66 |         streamer = FakeStreamer()
 67 |         streamer._pids['foobar'] = [1234, 1235]
 68 |         streamer._pids['barbaz'] = [1236, 1237]
 69 |         self.assertEqual(set(streamer.get_pids()),
 70 |                          set([1234, 1235, 1236, 1237]))
 71 | 
 72 |     @mock.patch('os.getpid', lambda: 2222)
 73 |     def test_get_circus_pids(self):
 74 |         def _send_message(message, name=None):
 75 |             if message == 'list':
 76 |                 if name == 'circushttpd':
 77 |                     return {'pids': [3333]}
 78 |                 return {'watchers': ['circushttpd']}
 79 | 
 80 |             if message == 'dstats':
 81 |                 return {'info': {'pid': 1111}}
 82 | 
 83 |         streamer = FakeStreamer()
 84 |         streamer.client = mock.MagicMock()
 85 |         streamer.client.send_message = _send_message
 86 | 
 87 |         self.assertEqual(
 88 |             streamer.get_circus_pids(),
 89 |             {1111: 'circusd', 2222: 'circusd-stats',
 90 |              3333: 'circushttpd'})
 91 | 
 92 |     def test_remove_pid(self):
 93 |         streamer = FakeStreamer()
 94 |         streamer._callbacks['foobar'] = mock.MagicMock()
 95 |         streamer._pids = {'foobar': [1234, 1235]}
 96 |         streamer.remove_pid('foobar', 1234)
 97 |         self.assertFalse(streamer._callbacks['foobar'].stop.called)
 98 | 
 99 |         streamer.remove_pid('foobar', 1235)
100 |         self.assertTrue(streamer._callbacks['foobar'].stop.called)
101 | 
102 | 
103 | 


--------------------------------------------------------------------------------
/circus/commands/options.py:
--------------------------------------------------------------------------------
  1 | from circus.commands.base import Command
  2 | from circus.exc import ArgumentError
  3 | from circus.util import convert_opt
  4 | 
  5 | 
  6 | class Options(Command):
  7 |     """\
  8 |         Get the value of all options for a watcher
  9 |         ==========================================
 10 | 
 11 |         This command returns all option values for a given watcher.
 12 | 
 13 |         ZMQ Message
 14 |         -----------
 15 | 
 16 |         ::
 17 | 
 18 |             {
 19 |                 "command": "options",
 20 |                 "properties": {
 21 |                     "name": "nameofwatcher",
 22 |                 }
 23 |             }
 24 | 
 25 |         A message contains 1 property:
 26 | 
 27 |         - name: name of watcher
 28 | 
 29 |         The response object has a property ``options`` which is a
 30 |         dictionary of option names and values.
 31 | 
 32 |         eg::
 33 | 
 34 |             {
 35 |                 "status": "ok",
 36 |                 "options": {
 37 |                     "graceful_timeout": 300,
 38 |                     "send_hup": True,
 39 |                     ...
 40 |                 },
 41 |                 time': 1332202594.754644
 42 |             }
 43 | 
 44 | 
 45 |         Command line
 46 |         ------------
 47 | 
 48 |         ::
 49 | 
 50 |             $ circusctl options 
 51 | 
 52 | 
 53 |         Options
 54 |         -------
 55 | 
 56 |         - : name of the watcher
 57 | 
 58 |         Options Keys are:
 59 | 
 60 |         - numprocesses: integer, number of processes
 61 |         - warmup_delay: integer or number, delay to wait between process
 62 |           spawning in seconds
 63 |         - working_dir: string, directory where the process will be executed
 64 |         - uid: string or integer, user ID used to launch the process
 65 |         - gid: string or integer, group ID used to launch the process
 66 |         - send_hup: boolean, if TRU the signal HUP will be used on reload
 67 |         - shell: boolean, will run the command in the shell environment if
 68 |           true
 69 |         - cmd: string, The command line used to launch the process
 70 |         - env: object, define the environnement in which the process will be
 71 |           launch
 72 |         - retry_in: integer or number, time in seconds we wait before we retry
 73 |           to launch the process if the maximum number of attempts
 74 |           has been reach.
 75 |         - max_retry: integer, The maximum of retries loops
 76 |         - graceful_timeout: integer or number, time we wait before we
 77 |           definitely kill a process.
 78 |         - priority: used to sort watchers in the arbiter
 79 |         - singleton: if True, a singleton watcher.
 80 |         - max_age: time a process can live before being restarted
 81 |         - max_age_variance: variable additional time to live, avoids
 82 |           stampeding herd.
 83 |     """
 84 | 
 85 |     name = "options"
 86 |     properties = ['name']
 87 | 
 88 |     def message(self, *args, **opts):
 89 | 
 90 |         if len(args) < 1:
 91 |             raise ArgumentError("number of arguments invalid")
 92 | 
 93 |         return self.make_message(name=args[0])
 94 | 
 95 |     def execute(self, arbiter, props):
 96 |         watcher = self._get_watcher(arbiter, props['name'])
 97 |         return {"options": dict(watcher.options())}
 98 | 
 99 |     def console_msg(self, msg):
100 |         if msg['status'] == "ok":
101 |             ret = []
102 |             for k, v in msg.get('options', {}).items():
103 |                 ret.append("%s: %s" % (k, convert_opt(k, v)))
104 |             return "\n".join(ret)
105 |         return self.console_error(msg)
106 | 


--------------------------------------------------------------------------------
/tests/test_plugin_command_reloader.py:
--------------------------------------------------------------------------------
 1 | from unittest.mock import patch
 2 | 
 3 | from circus.plugins.command_reloader import CommandReloader
 4 | from tests.support import TestCircus
 5 | 
 6 | 
 7 | class TestCommandReloader(TestCircus):
 8 | 
 9 |     def setup_os_mock(self, realpath, mtime):
10 |         patcher = patch('circus.plugins.command_reloader.os')
11 |         os_mock = patcher.start()
12 |         self.addCleanup(patcher.stop)
13 |         os_mock.path.realpath.return_value = realpath
14 |         os_mock.stat.return_value.st_mtime = mtime
15 |         return os_mock
16 | 
17 |     def setup_call_mock(self, watcher_name):
18 |         patcher = patch.object(CommandReloader, 'call')
19 |         call_mock = patcher.start()
20 |         self.addCleanup(patcher.stop)
21 |         call_mock.side_effect = [
22 |             {'watchers': [watcher_name]},
23 |             {'options': {'cmd': watcher_name}},
24 |             None,
25 |         ]
26 |         return call_mock
27 | 
28 |     def test_default_loop_rate(self):
29 |         plugin = self.make_plugin(CommandReloader, active=True)
30 |         self.assertEqual(plugin.loop_rate, 1)
31 | 
32 |     def test_non_default_loop_rate(self):
33 |         plugin = self.make_plugin(CommandReloader, active=True, loop_rate='2')
34 |         self.assertEqual(plugin.loop_rate, 2)
35 | 
36 |     def test_mtime_is_modified(self):
37 |         plugin = self.make_plugin(CommandReloader, active=True)
38 |         plugin.cmd_files = {'foo': {'path': '/bar/baz', 'mtime': 1}}
39 |         self.assertTrue(plugin.is_modified('foo', 2, '/bar/baz'))
40 | 
41 |     def test_path_is_modified(self):
42 |         plugin = self.make_plugin(CommandReloader, active=True)
43 |         plugin.cmd_files = {'foo': {'path': '/bar/baz', 'mtime': 1}}
44 |         self.assertTrue(plugin.is_modified('foo', 1, '/bar/quux'))
45 | 
46 |     def test_not_modified(self):
47 |         plugin = self.make_plugin(CommandReloader, active=True)
48 |         plugin.cmd_files = {'foo': {'path': '/bar/quux', 'mtime': 1}}
49 |         self.assertIs(plugin.is_modified('foo', 1, '/bar/quux'), False)
50 | 
51 |     def test_look_after_known_watcher_triggers_restart(self):
52 |         call_mock = self.setup_call_mock(watcher_name='foo')
53 |         self.setup_os_mock(realpath='/bar/foo', mtime=42)
54 |         plugin = self.make_plugin(CommandReloader, active=True)
55 |         plugin.cmd_files = {'foo': {'path': 'foo', 'mtime': 1}}
56 | 
57 |         plugin.look_after()
58 | 
59 |         self.assertEqual(plugin.cmd_files, {
60 |             'foo': {'path': '/bar/foo', 'mtime': 42}
61 |         })
62 |         call_mock.assert_called_with('restart', name='foo')
63 | 
64 |     def test_look_after_new_watcher_does_not_restart(self):
65 |         call_mock = self.setup_call_mock(watcher_name='foo')
66 |         self.setup_os_mock(realpath='/bar/foo', mtime=42)
67 |         plugin = self.make_plugin(CommandReloader, active=True)
68 |         plugin.cmd_files = {}
69 | 
70 |         plugin.look_after()
71 | 
72 |         self.assertEqual(plugin.cmd_files, {
73 |             'foo': {'path': '/bar/foo', 'mtime': 42}
74 |         })
75 |         # No restart, so last call should be for the 'get' command
76 |         call_mock.assert_called_with('get', name='foo', keys=['cmd'])
77 | 
78 |     def test_missing_watcher_gets_removed_from_plugin_dict(self):
79 |         self.setup_call_mock(watcher_name='bar')
80 |         self.setup_os_mock(realpath='/bar/foo', mtime=42)
81 |         plugin = self.make_plugin(CommandReloader, active=True)
82 |         plugin.cmd_files = {'foo': {'path': 'foo', 'mtime': 1}}
83 | 
84 |         plugin.look_after()
85 | 
86 |         self.assertNotIn('foo', plugin.cmd_files)
87 | 
88 |     def test_handle_recv_implemented(self):
89 |         plugin = self.make_plugin(CommandReloader, active=True)
90 |         plugin.handle_recv('whatever')
91 | 
92 | 
93 | 


--------------------------------------------------------------------------------
/docs/source/for-ops/sockets.rst:
--------------------------------------------------------------------------------
  1 | .. _sockets:
  2 | 
  3 | Working with sockets
  4 | ####################
  5 | 
  6 | Circus can bind network sockets and manage them as it does for processes.
  7 | 
  8 | The main idea is that a child process that's created by Circus to run one of
  9 | the watcher's command can inherit from all the opened file descriptors.
 10 | 
 11 | That's how Apache or Unicorn works, and many other tools out there.
 12 | 
 13 | Goal
 14 | ====
 15 | 
 16 | The goal of having sockets managed by Circus is to be able to manage network
 17 | applications in Circus exactly like other applications.
 18 | 
 19 | For example, if you use Circus with `Chaussette `_
 20 | -- a WGSI server, you can get a very fast web server running and manage
 21 | *"Web Workers"* in Circus as you would do for any other process.
 22 | 
 23 | Splitting the socket managment from the network application itself offers
 24 | a lot of opportunities to scale and manage your stack.
 25 | 
 26 | 
 27 | Design
 28 | ======
 29 | 
 30 | The gist of the feature is done by binding the socket and start listening
 31 | to it in **circusd**:
 32 | 
 33 | .. code-block:: python
 34 | 
 35 |     import socket
 36 | 
 37 |     sock = socket.socket(FAMILY, TYPE)
 38 |     sock.bind((HOST, PORT))
 39 |     sock.listen(BACKLOG)
 40 |     fd = sock.fileno()
 41 | 
 42 | 
 43 | Circus then keeps track of all the opened fds, and let the processes it
 44 | runs as children have access to them if they want.
 45 | 
 46 | If you create a small Python network script that you intend to run in Circus,
 47 | it could look like this:
 48 | 
 49 | .. code-block:: python
 50 | 
 51 |     import socket
 52 |     import sys
 53 | 
 54 |     fd = int(sys.argv[1])   # getting the FD from circus
 55 |     sock = socket.fromfd(fd, FAMILY, TYPE)
 56 | 
 57 |     # dealing with one request at a time
 58 |     while True:
 59 |         conn, addr = sock.accept()
 60 |         request = conn.recv(1024)
 61 |         .. do something ..
 62 |         conn.sendall(response)
 63 |         conn.close()
 64 | 
 65 | 
 66 | Then Circus could run like this:
 67 | 
 68 | .. code-block:: ini
 69 | 
 70 |     [circus]
 71 |     check_delay = 5
 72 |     endpoint = tcp://127.0.0.1:5555
 73 |     pubsub_endpoint = tcp://127.0.0.1:5556
 74 |     stats_endpoint = tcp://127.0.0.1:5557
 75 | 
 76 |     [watcher:dummy]
 77 |     cmd = mycoolscript $(circus.sockets.foo)
 78 |     use_sockets = True
 79 |     warmup_delay = 0
 80 |     numprocesses = 5
 81 | 
 82 |     [socket:foo]
 83 |     host = 127.0.0.1
 84 |     port = 8888
 85 | 
 86 | *$(circus.sockets.foo)* will be replaced by the FD value once the socket is
 87 | created and bound on the 8888 *port*.
 88 | 
 89 | .. note::
 90 | 
 91 |    Starting at Circus 0.8 there's an alternate syntax to avoid some
 92 |    conflicts with some config parsers. You can write::
 93 | 
 94 |        ((circus.sockets.foo))
 95 | 
 96 | 
 97 | Real-world example
 98 | ==================
 99 | 
100 | `Chaussette `_ is the perfect Circus companion if
101 | you want to run your WSGI application.
102 | 
103 | Once it's installed, running 5 **meinheld** workers can be done by creating a
104 | socket and calling the **chaussette** command in a worker, like this:
105 | 
106 | .. code-block:: ini
107 | 
108 |     [circus]
109 |     endpoint = tcp://127.0.0.1:5555
110 |     pubsub_endpoint = tcp://127.0.0.1:5556
111 |     stats_endpoint = tcp://127.0.0.1:5557
112 | 
113 |     [watcher:web]
114 |     cmd = chaussette --fd $(circus.sockets.web) --backend meinheld mycool.app
115 |     use_sockets = True
116 |     numprocesses = 5
117 | 
118 |     [socket:web]
119 |     host = 0.0.0.0
120 |     port = 8000
121 | 
122 | 
123 | We did not publish benchmarks yet, but a Web cluster managed by Circus with a Gevent
124 | or Meinheld backend is as fast as any pre-fork WSGI server out there.
125 | 


--------------------------------------------------------------------------------