Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 1 | # |
| 2 | # Licensed to the Apache Software Foundation (ASF) under one |
| 3 | # or more contributor license agreements. See the NOTICE file |
| 4 | # distributed with this work for additional information |
| 5 | # regarding copyright ownership. The ASF licenses this file |
| 6 | # to you under the Apache License, Version 2.0 (the |
| 7 | # "License"); you may not use this file except in compliance |
| 8 | # with the License. You may obtain a copy of the License at |
| 9 | # |
| 10 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | # |
| 12 | # Unless required by applicable law or agreed to in writing, |
| 13 | # software distributed under the License is distributed on an |
| 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 15 | # KIND, either express or implied. See the License for the |
| 16 | # specific language governing permissions and limitations |
| 17 | # under the License. |
| 18 | # |
| 19 | |
| 20 | import contextlib |
| 21 | import multiprocessing |
| 22 | import multiprocessing.managers |
| 23 | import os |
| 24 | import platform |
| 25 | import random |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 26 | import signal |
Nobuaki Sukegawa | a6ab1f5 | 2015-11-28 15:04:39 +0900 | [diff] [blame] | 27 | import socket |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 28 | import subprocess |
Nobuaki Sukegawa | a6ab1f5 | 2015-11-28 15:04:39 +0900 | [diff] [blame] | 29 | import sys |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 30 | import threading |
| 31 | import time |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 32 | |
Nobuaki Sukegawa | 2de2700 | 2015-11-22 01:13:48 +0900 | [diff] [blame] | 33 | from .compat import str_join |
| 34 | from .test import TestEntry, domain_socket_path |
| 35 | from .report import ExecReporter, SummaryReporter |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 36 | |
| 37 | RESULT_TIMEOUT = 128 |
| 38 | RESULT_ERROR = 64 |
| 39 | |
| 40 | |
| 41 | class ExecutionContext(object): |
| 42 | def __init__(self, cmd, cwd, env, report): |
| 43 | self._log = multiprocessing.get_logger() |
| 44 | self.report = report |
| 45 | self.cmd = cmd |
| 46 | self.cwd = cwd |
| 47 | self.env = env |
| 48 | self.timer = None |
| 49 | self.expired = False |
Nobuaki Sukegawa | 2ba7944 | 2016-01-12 19:37:55 +0900 | [diff] [blame^] | 50 | self.killed = False |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 51 | |
| 52 | def _expire(self): |
| 53 | self._log.info('Timeout') |
| 54 | self.expired = True |
| 55 | self.kill() |
| 56 | |
| 57 | def kill(self): |
| 58 | self._log.debug('Killing process : %d' % self.proc.pid) |
Nobuaki Sukegawa | 2ba7944 | 2016-01-12 19:37:55 +0900 | [diff] [blame^] | 59 | self.killed = True |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 60 | if platform.system() != 'Windows': |
| 61 | try: |
| 62 | os.killpg(self.proc.pid, signal.SIGKILL) |
| 63 | except Exception as err: |
| 64 | self._log.info('Failed to kill process group : %s' % str(err)) |
| 65 | try: |
| 66 | self.proc.kill() |
| 67 | except Exception as err: |
| 68 | self._log.info('Failed to kill process : %s' % str(err)) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 69 | |
| 70 | def _popen_args(self): |
| 71 | args = { |
| 72 | 'cwd': self.cwd, |
| 73 | 'env': self.env, |
| 74 | 'stdout': self.report.out, |
| 75 | 'stderr': subprocess.STDOUT, |
| 76 | } |
| 77 | # make sure child processes doesn't remain after killing |
| 78 | if platform.system() == 'Windows': |
| 79 | DETACHED_PROCESS = 0x00000008 |
| 80 | args.update(creationflags=DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP) |
| 81 | else: |
| 82 | args.update(preexec_fn=os.setsid) |
| 83 | return args |
| 84 | |
| 85 | def start(self, timeout=0): |
Nobuaki Sukegawa | 2de2700 | 2015-11-22 01:13:48 +0900 | [diff] [blame] | 86 | joined = str_join(' ', self.cmd) |
Jens Geyer | aad06de | 2015-11-21 14:43:56 +0100 | [diff] [blame] | 87 | self._log.debug('COMMAND: %s', joined) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 88 | self._log.debug('WORKDIR: %s', self.cwd) |
| 89 | self._log.debug('LOGFILE: %s', self.report.logpath) |
| 90 | self.report.begin() |
Nobuaki Sukegawa | 2de2700 | 2015-11-22 01:13:48 +0900 | [diff] [blame] | 91 | self.proc = subprocess.Popen(self.cmd, **self._popen_args()) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 92 | if timeout > 0: |
| 93 | self.timer = threading.Timer(timeout, self._expire) |
| 94 | self.timer.start() |
| 95 | return self._scoped() |
| 96 | |
| 97 | @contextlib.contextmanager |
| 98 | def _scoped(self): |
| 99 | yield self |
| 100 | self._log.debug('Killing scoped process') |
Nobuaki Sukegawa | 2ba7944 | 2016-01-12 19:37:55 +0900 | [diff] [blame^] | 101 | if self.proc.poll() is None: |
| 102 | self.kill() |
| 103 | self.report.killed() |
| 104 | else: |
| 105 | self._log.debug('Process died unexpectedly') |
| 106 | self.report.died() |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 107 | |
| 108 | def wait(self): |
| 109 | self.proc.communicate() |
| 110 | if self.timer: |
| 111 | self.timer.cancel() |
| 112 | self.report.end(self.returncode) |
| 113 | |
| 114 | @property |
| 115 | def returncode(self): |
| 116 | return self.proc.returncode if self.proc else None |
| 117 | |
| 118 | |
Nobuaki Sukegawa | 378b727 | 2016-01-03 17:04:50 +0900 | [diff] [blame] | 119 | def exec_context(port, logdir, test, prog): |
| 120 | report = ExecReporter(logdir, test, prog) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 121 | prog.build_command(port) |
| 122 | return ExecutionContext(prog.command, prog.workdir, prog.env, report) |
| 123 | |
| 124 | |
Nobuaki Sukegawa | 378b727 | 2016-01-03 17:04:50 +0900 | [diff] [blame] | 125 | def run_test(testdir, logdir, test_dict, async=True, max_retry=3): |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 126 | try: |
| 127 | logger = multiprocessing.get_logger() |
| 128 | retry_count = 0 |
| 129 | test = TestEntry(testdir, **test_dict) |
| 130 | while True: |
| 131 | if stop.is_set(): |
| 132 | logger.debug('Skipping because shutting down') |
| 133 | return None |
| 134 | logger.debug('Start') |
| 135 | with PortAllocator.alloc_port_scoped(ports, test.socket) as port: |
| 136 | logger.debug('Start with port %d' % port) |
Nobuaki Sukegawa | 378b727 | 2016-01-03 17:04:50 +0900 | [diff] [blame] | 137 | sv = exec_context(port, logdir, test, test.server) |
| 138 | cl = exec_context(port, logdir, test, test.client) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 139 | |
| 140 | logger.debug('Starting server') |
| 141 | with sv.start(): |
| 142 | if test.delay > 0: |
| 143 | logger.debug('Delaying client for %.2f seconds' % test.delay) |
| 144 | time.sleep(test.delay) |
| 145 | cl_retry_count = 0 |
| 146 | cl_max_retry = 10 |
| 147 | cl_retry_wait = 0.5 |
| 148 | while True: |
| 149 | logger.debug('Starting client') |
| 150 | cl.start(test.timeout) |
| 151 | logger.debug('Waiting client') |
| 152 | cl.wait() |
| 153 | if not cl.report.maybe_false_positive() or cl_retry_count >= cl_max_retry: |
| 154 | if cl_retry_count > 0 and cl_retry_count < cl_max_retry: |
| 155 | logger.warn('[%s]: Connected after %d retry (%.2f sec each)' % (test.server.name, cl_retry_count, cl_retry_wait)) |
Nobuaki Sukegawa | 2ba7944 | 2016-01-12 19:37:55 +0900 | [diff] [blame^] | 156 | # Wait for 50 ms to see if server does not die at the end. |
| 157 | time.sleep(0.05) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 158 | break |
| 159 | logger.debug('Server may not be ready, waiting %.2f second...' % cl_retry_wait) |
| 160 | time.sleep(cl_retry_wait) |
| 161 | cl_retry_count += 1 |
| 162 | |
| 163 | if not sv.report.maybe_false_positive() or retry_count >= max_retry: |
| 164 | logger.debug('Finish') |
Nobuaki Sukegawa | 2ba7944 | 2016-01-12 19:37:55 +0900 | [diff] [blame^] | 165 | if cl.expired: |
| 166 | return RESULT_TIMEOUT |
| 167 | elif not sv.killed and cl.proc.returncode == 0: |
| 168 | # Server should be alive at the end. |
| 169 | return RESULT_ERROR |
| 170 | else: |
| 171 | return cl.proc.returncode |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 172 | logger.warn('[%s]: Detected socket bind failure, retrying...' % test.server.name) |
| 173 | retry_count += 1 |
| 174 | except (KeyboardInterrupt, SystemExit): |
| 175 | logger.info('Interrupted execution') |
| 176 | if not async: |
| 177 | raise |
| 178 | stop.set() |
| 179 | return None |
Nobuaki Sukegawa | 2ba7944 | 2016-01-12 19:37:55 +0900 | [diff] [blame^] | 180 | except: |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 181 | if not async: |
| 182 | raise |
Nobuaki Sukegawa | 2ba7944 | 2016-01-12 19:37:55 +0900 | [diff] [blame^] | 183 | logger.warn('Error executing [%s]', test.name, exc_info=sys.exc_info()) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 184 | return RESULT_ERROR |
| 185 | |
| 186 | |
| 187 | class PortAllocator(object): |
| 188 | def __init__(self): |
| 189 | self._log = multiprocessing.get_logger() |
| 190 | self._lock = multiprocessing.Lock() |
| 191 | self._ports = set() |
| 192 | self._dom_ports = set() |
| 193 | self._last_alloc = 0 |
| 194 | |
| 195 | def _get_tcp_port(self): |
| 196 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 197 | sock.bind(('127.0.0.1', 0)) |
| 198 | port = sock.getsockname()[1] |
| 199 | self._lock.acquire() |
| 200 | try: |
| 201 | ok = port not in self._ports |
| 202 | if ok: |
| 203 | self._ports.add(port) |
| 204 | self._last_alloc = time.time() |
| 205 | finally: |
| 206 | self._lock.release() |
| 207 | sock.close() |
| 208 | return port if ok else self._get_tcp_port() |
| 209 | |
| 210 | def _get_domain_port(self): |
| 211 | port = random.randint(1024, 65536) |
| 212 | self._lock.acquire() |
| 213 | try: |
| 214 | ok = port not in self._dom_ports |
| 215 | if ok: |
| 216 | self._dom_ports.add(port) |
| 217 | finally: |
| 218 | self._lock.release() |
| 219 | return port if ok else self._get_domain_port() |
| 220 | |
| 221 | def alloc_port(self, socket_type): |
pavlo | dd08f6e | 2015-10-08 16:43:56 -0400 | [diff] [blame] | 222 | if socket_type in ('domain', 'abstract'): |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 223 | return self._get_domain_port() |
| 224 | else: |
| 225 | return self._get_tcp_port() |
| 226 | |
| 227 | # static method for inter-process invokation |
| 228 | @staticmethod |
| 229 | @contextlib.contextmanager |
| 230 | def alloc_port_scoped(allocator, socket_type): |
| 231 | port = allocator.alloc_port(socket_type) |
| 232 | yield port |
| 233 | allocator.free_port(socket_type, port) |
| 234 | |
| 235 | def free_port(self, socket_type, port): |
| 236 | self._log.debug('free_port') |
| 237 | self._lock.acquire() |
| 238 | try: |
| 239 | if socket_type == 'domain': |
| 240 | self._dom_ports.remove(port) |
| 241 | path = domain_socket_path(port) |
| 242 | if os.path.exists(path): |
| 243 | os.remove(path) |
pavlo | dd08f6e | 2015-10-08 16:43:56 -0400 | [diff] [blame] | 244 | elif socket_type == 'abstract': |
| 245 | self._dom_ports.remove(port) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 246 | else: |
| 247 | self._ports.remove(port) |
| 248 | except IOError as err: |
| 249 | self._log.info('Error while freeing port : %s' % str(err)) |
| 250 | finally: |
| 251 | self._lock.release() |
| 252 | |
| 253 | |
| 254 | class NonAsyncResult(object): |
| 255 | def __init__(self, value): |
| 256 | self._value = value |
| 257 | |
| 258 | def get(self, timeout=None): |
| 259 | return self._value |
| 260 | |
| 261 | def wait(self, timeout=None): |
| 262 | pass |
| 263 | |
| 264 | def ready(self): |
| 265 | return True |
| 266 | |
| 267 | def successful(self): |
| 268 | return self._value == 0 |
| 269 | |
| 270 | |
| 271 | class TestDispatcher(object): |
Nobuaki Sukegawa | 378b727 | 2016-01-03 17:04:50 +0900 | [diff] [blame] | 272 | def __init__(self, testdir, logdir, concurrency): |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 273 | self._log = multiprocessing.get_logger() |
| 274 | self.testdir = testdir |
Nobuaki Sukegawa | 378b727 | 2016-01-03 17:04:50 +0900 | [diff] [blame] | 275 | self.logdir = logdir |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 276 | # seems needed for python 2.x to handle keyboard interrupt |
| 277 | self._stop = multiprocessing.Event() |
| 278 | self._async = concurrency > 1 |
| 279 | if not self._async: |
| 280 | self._pool = None |
| 281 | global stop |
| 282 | global ports |
| 283 | stop = self._stop |
| 284 | ports = PortAllocator() |
| 285 | else: |
| 286 | self._m = multiprocessing.managers.BaseManager() |
| 287 | self._m.register('ports', PortAllocator) |
| 288 | self._m.start() |
| 289 | self._pool = multiprocessing.Pool(concurrency, self._pool_init, (self._m.address,)) |
Nobuaki Sukegawa | 378b727 | 2016-01-03 17:04:50 +0900 | [diff] [blame] | 290 | self._report = SummaryReporter(logdir, concurrency > 1) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 291 | self._log.debug( |
| 292 | 'TestDispatcher started with %d concurrent jobs' % concurrency) |
| 293 | |
| 294 | def _pool_init(self, address): |
| 295 | global stop |
| 296 | global m |
| 297 | global ports |
| 298 | stop = self._stop |
| 299 | m = multiprocessing.managers.BaseManager(address) |
| 300 | m.connect() |
| 301 | ports = m.ports() |
| 302 | |
| 303 | def _dispatch_sync(self, test, cont): |
Nobuaki Sukegawa | 378b727 | 2016-01-03 17:04:50 +0900 | [diff] [blame] | 304 | r = run_test(self.testdir, self.logdir, test, False) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 305 | cont(r) |
| 306 | return NonAsyncResult(r) |
| 307 | |
| 308 | def _dispatch_async(self, test, cont): |
Nobuaki Sukegawa | 378b727 | 2016-01-03 17:04:50 +0900 | [diff] [blame] | 309 | self._log.debug('_dispatch_async') |
| 310 | return self._pool.apply_async(func=run_test, args=(self.testdir, self.logdir, test,), callback=cont) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 311 | |
| 312 | def dispatch(self, test): |
| 313 | index = self._report.add_test(test) |
| 314 | |
| 315 | def cont(r): |
| 316 | if not self._stop.is_set(): |
| 317 | self._log.debug('freeing port') |
| 318 | self._log.debug('adding result') |
| 319 | self._report.add_result(index, r, r == RESULT_TIMEOUT) |
| 320 | self._log.debug('finish continuation') |
| 321 | fn = self._dispatch_async if self._async else self._dispatch_sync |
| 322 | return fn(test, cont) |
| 323 | |
| 324 | def wait(self): |
| 325 | if self._async: |
| 326 | self._pool.close() |
| 327 | self._pool.join() |
| 328 | self._m.shutdown() |
| 329 | return self._report.end() |
| 330 | |
| 331 | def terminate(self): |
| 332 | self._stop.set() |
| 333 | if self._async: |
| 334 | self._pool.terminate() |
| 335 | self._pool.join() |
| 336 | self._m.shutdown() |