|
| 1 | +import os |
| 2 | +import re |
| 3 | +import signal |
| 4 | +from subprocess import TimeoutExpired |
| 5 | +import sys |
| 6 | +import time |
| 7 | + |
| 8 | +import pytest |
| 9 | + |
| 10 | +from tests.contrib.uwsgi import run_uwsgi |
| 11 | +from tests.profiling.collector import pprof_utils |
| 12 | + |
| 13 | + |
| 14 | +# uwsgi is not available on Windows |
| 15 | +if sys.platform == "win32": |
| 16 | + pytestmark = pytest.mark.skip |
| 17 | + |
| 18 | +TESTING_GEVENT = os.getenv("DD_PROFILE_TEST_GEVENT", False) |
| 19 | +THREADS_MSG = ( |
| 20 | + b"ddtrace.internal.uwsgi.uWSGIConfigError: enable-threads option must be set to true, or a positive " |
| 21 | + b"number of threads must be set" |
| 22 | +) |
| 23 | + |
| 24 | +uwsgi_app = os.path.join(os.path.dirname(__file__), "..", "profiling", "uwsgi-app.py") |
| 25 | + |
| 26 | + |
| 27 | +@pytest.fixture |
| 28 | +def uwsgi(monkeypatch, tmp_path): |
| 29 | + # Do not ignore profiler so we have samples in the output pprof |
| 30 | + monkeypatch.setenv("DD_PROFILING_IGNORE_PROFILER", "0") |
| 31 | + # Do not use pytest tmpdir fixtures which generate directories longer than allowed for a socket file name |
| 32 | + socket_name = str(tmp_path / "uwsgi.sock") |
| 33 | + import os |
| 34 | + |
| 35 | + cmd = [ |
| 36 | + "uwsgi", |
| 37 | + "--need-app", |
| 38 | + "--die-on-term", |
| 39 | + "--socket", |
| 40 | + socket_name, |
| 41 | + "--wsgi-file", |
| 42 | + uwsgi_app, |
| 43 | + ] |
| 44 | + |
| 45 | + try: |
| 46 | + yield run_uwsgi(cmd) |
| 47 | + finally: |
| 48 | + os.unlink(socket_name) |
| 49 | + |
| 50 | + |
| 51 | +def test_uwsgi_threads_disabled(uwsgi): |
| 52 | + proc = uwsgi() |
| 53 | + stdout, _ = proc.communicate() |
| 54 | + assert proc.wait() != 0 |
| 55 | + assert THREADS_MSG in stdout |
| 56 | + |
| 57 | + |
| 58 | +def test_uwsgi_threads_number_set(uwsgi): |
| 59 | + proc = uwsgi("--threads", "1") |
| 60 | + try: |
| 61 | + stdout, _ = proc.communicate(timeout=1) |
| 62 | + except TimeoutExpired: |
| 63 | + proc.terminate() |
| 64 | + stdout, _ = proc.communicate() |
| 65 | + assert THREADS_MSG not in stdout |
| 66 | + |
| 67 | + |
| 68 | +def test_uwsgi_threads_enabled(uwsgi, tmp_path, monkeypatch): |
| 69 | + filename = str(tmp_path / "uwsgi.pprof") |
| 70 | + monkeypatch.setenv("DD_PROFILING_OUTPUT_PPROF", filename) |
| 71 | + proc = uwsgi("--enable-threads") |
| 72 | + worker_pids = _get_worker_pids(proc.stdout, 1) |
| 73 | + # Give some time to the process to actually startup |
| 74 | + time.sleep(3) |
| 75 | + proc.terminate() |
| 76 | + assert proc.wait() == 30 |
| 77 | + for pid in worker_pids: |
| 78 | + profile = pprof_utils.parse_profile("%s.%d" % (filename, pid)) |
| 79 | + samples = pprof_utils.get_samples_with_value_type(profile, "wall-time") |
| 80 | + assert len(samples) > 0 |
| 81 | + |
| 82 | + |
| 83 | +def test_uwsgi_threads_processes_no_primary(uwsgi, monkeypatch): |
| 84 | + proc = uwsgi("--enable-threads", "--processes", "2") |
| 85 | + stdout, _ = proc.communicate() |
| 86 | + assert ( |
| 87 | + b"ddtrace.internal.uwsgi.uWSGIConfigError: master option must be enabled when multiple processes are used" |
| 88 | + in stdout |
| 89 | + ) |
| 90 | + |
| 91 | + |
| 92 | +def _get_worker_pids(stdout, num_worker, num_app_started=1): |
| 93 | + worker_pids = [] |
| 94 | + started = 0 |
| 95 | + while True: |
| 96 | + line = stdout.readline() |
| 97 | + if line == b"": |
| 98 | + break |
| 99 | + elif b"WSGI app 0 (mountpoint='') ready" in line: |
| 100 | + started += 1 |
| 101 | + else: |
| 102 | + m = re.match(r"^spawned uWSGI worker \d+ .*\(pid: (\d+),", line.decode()) |
| 103 | + if m: |
| 104 | + worker_pids.append(int(m.group(1))) |
| 105 | + |
| 106 | + if len(worker_pids) == num_worker and num_app_started == started: |
| 107 | + break |
| 108 | + |
| 109 | + return worker_pids |
| 110 | + |
| 111 | + |
| 112 | +def test_uwsgi_threads_processes_primary(uwsgi, tmp_path, monkeypatch): |
| 113 | + filename = str(tmp_path / "uwsgi.pprof") |
| 114 | + monkeypatch.setenv("DD_PROFILING_OUTPUT_PPROF", filename) |
| 115 | + proc = uwsgi("--enable-threads", "--master", "--py-call-uwsgi-fork-hooks", "--processes", "2") |
| 116 | + worker_pids = _get_worker_pids(proc.stdout, 2) |
| 117 | + # Give some time to child to actually startup |
| 118 | + time.sleep(3) |
| 119 | + proc.terminate() |
| 120 | + assert proc.wait() == 0 |
| 121 | + for pid in worker_pids: |
| 122 | + profile = pprof_utils.parse_profile("%s.%d" % (filename, pid)) |
| 123 | + samples = pprof_utils.get_samples_with_value_type(profile, "wall-time") |
| 124 | + assert len(samples) > 0 |
| 125 | + |
| 126 | + |
| 127 | +def test_uwsgi_threads_processes_primary_lazy_apps(uwsgi, tmp_path, monkeypatch): |
| 128 | + filename = str(tmp_path / "uwsgi.pprof") |
| 129 | + monkeypatch.setenv("DD_PROFILING_OUTPUT_PPROF", filename) |
| 130 | + monkeypatch.setenv("DD_PROFILING_UPLOAD_INTERVAL", "1") |
| 131 | + # For uwsgi<2.0.30, --skip-atexit is required to avoid crashes when |
| 132 | + # the child process exits. |
| 133 | + proc = uwsgi("--enable-threads", "--master", "--processes", "2", "--lazy-apps", "--skip-atexit") |
| 134 | + worker_pids = _get_worker_pids(proc.stdout, 2, 2) |
| 135 | + # Give some time to child to actually startup and output a profile |
| 136 | + time.sleep(3) |
| 137 | + proc.terminate() |
| 138 | + assert proc.wait() == 0 |
| 139 | + for pid in worker_pids: |
| 140 | + profile = pprof_utils.parse_profile("%s.%d" % (filename, pid)) |
| 141 | + samples = pprof_utils.get_samples_with_value_type(profile, "wall-time") |
| 142 | + assert len(samples) > 0 |
| 143 | + |
| 144 | + |
| 145 | +def test_uwsgi_threads_processes_no_primary_lazy_apps(uwsgi, tmp_path, monkeypatch): |
| 146 | + filename = str(tmp_path / "uwsgi.pprof") |
| 147 | + monkeypatch.setenv("DD_PROFILING_OUTPUT_PPROF", filename) |
| 148 | + monkeypatch.setenv("DD_PROFILING_UPLOAD_INTERVAL", "1") |
| 149 | + # For uwsgi<2.0.30, --skip-atexit is required to avoid crashes when |
| 150 | + # the child process exits. |
| 151 | + proc = uwsgi("--enable-threads", "--processes", "2", "--lazy-apps", "--skip-atexit") |
| 152 | + worker_pids = _get_worker_pids(proc.stdout, 2, 2) |
| 153 | + # Give some time to child to actually startup and output a profile |
| 154 | + time.sleep(3) |
| 155 | + # The processes are started without a master/parent so killing one does not kill the other: |
| 156 | + # Kill them all and wait until they die. |
| 157 | + for pid in worker_pids: |
| 158 | + os.kill(pid, signal.SIGTERM) |
| 159 | + # The first worker is our child, we can wait for it "normally" |
| 160 | + os.waitpid(worker_pids[0], 0) |
| 161 | + # The other ones are grandchildren, we can't wait for it with `waitpid` |
| 162 | + for pid in worker_pids[1:]: |
| 163 | + # Wait for the uwsgi workers to all die |
| 164 | + while True: |
| 165 | + try: |
| 166 | + os.kill(pid, 0) |
| 167 | + except OSError: |
| 168 | + break |
| 169 | + for pid in worker_pids: |
| 170 | + profile = pprof_utils.parse_profile("%s.%d" % (filename, pid)) |
| 171 | + samples = pprof_utils.get_samples_with_value_type(profile, "wall-time") |
| 172 | + assert len(samples) > 0 |
0 commit comments