├── 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 |
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 | 
2 | 
3 | 
4 | 
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 |
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 |
--------------------------------------------------------------------------------