blob: 4eea02118e54682f5c6e26b36f53c7fdaa11966f [file] [log] [blame]
Siddharth Shukla8e64d902017-03-12 19:50:18 +01001#!/usr/bin/env python
Craig Tiller6169d5f2016-03-31 07:46:18 -07002# Copyright 2015, Google Inc.
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -08003# All rights reserved.
4#
5# Redistribution and use in source and binary forms, with or without
6# modification, are permitted provided that the following conditions are
7# met:
8#
9# * Redistributions of source code must retain the above copyright
10# notice, this list of conditions and the following disclaimer.
11# * Redistributions in binary form must reproduce the above
12# copyright notice, this list of conditions and the following disclaimer
13# in the documentation and/or other materials provided with the
14# distribution.
15# * Neither the name of Google Inc. nor the names of its
16# contributors may be used to endorse or promote products derived from
17# this software without specific prior written permission.
18#
19# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30"""Run stress test in C++"""
31
siddharthshukla0589e532016-07-07 16:08:01 +020032from __future__ import print_function
33
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -080034import argparse
35import atexit
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -080036import itertools
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -080037import json
38import multiprocessing
39import os
40import re
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -080041import subprocess
42import sys
43import tempfile
44import time
45import uuid
Siddharth Shuklad194f592017-03-11 19:12:43 +010046import six
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -080047
Jan Tattermusch5c79a312016-12-20 11:02:50 +010048import python_utils.dockerjob as dockerjob
49import python_utils.jobset as jobset
50
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -080051# Docker doesn't clean up after itself, so we do it on exit.
52atexit.register(lambda: subprocess.call(['stty', 'echo']))
53
54ROOT = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '../..'))
55os.chdir(ROOT)
56
57_DEFAULT_SERVER_PORT = 8080
58_DEFAULT_METRICS_PORT = 8081
59_DEFAULT_TEST_CASES = 'empty_unary:20,large_unary:20,client_streaming:20,server_streaming:20,empty_stream:20'
60_DEFAULT_NUM_CHANNELS_PER_SERVER = 5
61_DEFAULT_NUM_STUBS_PER_CHANNEL = 10
62
63# 15 mins default
Sree Kuchibhotla66977602016-01-04 14:34:11 -080064_DEFAULT_TEST_DURATION_SECS = 900
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -080065
66class CXXLanguage:
67
68 def __init__(self):
69 self.client_cwd = None
70 self.server_cwd = None
71 self.safename = 'cxx'
72
73 def client_cmd(self, args):
74 return ['bins/opt/stress_test'] + args
75
76 def server_cmd(self, args):
77 return ['bins/opt/interop_server'] + args
78
79 def global_env(self):
80 return {}
81
82 def __str__(self):
83 return 'c++'
84
85
86_LANGUAGES = {'c++': CXXLanguage(),}
87
88# languages supported as cloud_to_cloud servers
89_SERVERS = ['c++']
90
91DOCKER_WORKDIR_ROOT = '/var/local/git/grpc'
92
93
94def docker_run_cmdline(cmdline, image, docker_args=[], cwd=None, environ=None):
95 """Wraps given cmdline array to create 'docker run' cmdline from it."""
96 docker_cmdline = ['docker', 'run', '-i', '--rm=true']
97
98 # turn environ into -e docker args
99 if environ:
siddharthshukla0589e532016-07-07 16:08:01 +0200100 for k, v in environ.items():
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800101 docker_cmdline += ['-e', '%s=%s' % (k, v)]
102
103 # set working directory
104 workdir = DOCKER_WORKDIR_ROOT
105 if cwd:
106 workdir = os.path.join(workdir, cwd)
107 docker_cmdline += ['-w', workdir]
108
109 docker_cmdline += docker_args + [image] + cmdline
110 return docker_cmdline
111
112
113def bash_login_cmdline(cmdline):
114 """Creates bash -l -c cmdline from args list."""
115 # Use login shell:
116 # * rvm and nvm require it
117 # * makes error messages clearer if executables are missing
118 return ['bash', '-l', '-c', ' '.join(cmdline)]
119
120
121def _job_kill_handler(job):
122 if job._spec.container_name:
123 dockerjob.docker_kill(job._spec.container_name)
124 # When the job times out and we decide to kill it,
125 # we need to wait a before restarting the job
126 # to prevent "container name already in use" error.
127 # TODO(jtattermusch): figure out a cleaner way to to this.
128 time.sleep(2)
129
130
131def cloud_to_cloud_jobspec(language,
132 test_cases,
133 server_addresses,
134 test_duration_secs,
135 num_channels_per_server,
136 num_stubs_per_channel,
137 metrics_port,
138 docker_image=None):
139 """Creates jobspec for cloud-to-cloud interop test"""
140 cmdline = bash_login_cmdline(language.client_cmd([
141 '--test_cases=%s' % test_cases, '--server_addresses=%s' %
142 server_addresses, '--test_duration_secs=%s' % test_duration_secs,
143 '--num_stubs_per_channel=%s' % num_stubs_per_channel,
144 '--num_channels_per_server=%s' % num_channels_per_server,
145 '--metrics_port=%s' % metrics_port
146 ]))
siddharthshukla0589e532016-07-07 16:08:01 +0200147 print(cmdline)
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800148 cwd = language.client_cwd
149 environ = language.global_env()
150 if docker_image:
151 container_name = dockerjob.random_name('interop_client_%s' %
152 language.safename)
153 cmdline = docker_run_cmdline(
154 cmdline,
155 image=docker_image,
156 environ=environ,
157 cwd=cwd,
158 docker_args=['--net=host', '--name', container_name])
159 cwd = None
160
161 test_job = jobset.JobSpec(cmdline=cmdline,
162 cwd=cwd,
163 environ=environ,
164 shortname='cloud_to_cloud:%s:%s_server:stress_test' % (
165 language, server_name),
166 timeout_seconds=test_duration_secs * 2,
Sree Kuchibhotlae3717422016-01-04 10:14:38 -0800167 flake_retries=0,
168 timeout_retries=0,
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800169 kill_handler=_job_kill_handler)
170 test_job.container_name = container_name
171 return test_job
172
173
174def server_jobspec(language, docker_image, test_duration_secs):
175 """Create jobspec for running a server"""
176 container_name = dockerjob.random_name('interop_server_%s' %
177 language.safename)
178 cmdline = bash_login_cmdline(language.server_cmd(['--port=%s' %
179 _DEFAULT_SERVER_PORT]))
180 environ = language.global_env()
181 docker_cmdline = docker_run_cmdline(
182 cmdline,
183 image=docker_image,
184 cwd=language.server_cwd,
185 environ=environ,
186 docker_args=['-p', str(_DEFAULT_SERVER_PORT), '--name', container_name])
187
188 server_job = jobset.JobSpec(cmdline=docker_cmdline,
189 environ=environ,
190 shortname='interop_server_%s' % language,
191 timeout_seconds=test_duration_secs * 3)
192 server_job.container_name = container_name
193 return server_job
194
195
Sree Kuchibhotlae3717422016-01-04 10:14:38 -0800196def build_interop_stress_image_jobspec(language, tag=None):
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800197 """Creates jobspec for building stress test docker image for a language"""
198 if not tag:
Sree Kuchibhotlae3717422016-01-04 10:14:38 -0800199 tag = 'grpc_interop_stress_%s:%s' % (language.safename, uuid.uuid4())
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800200 env = {'INTEROP_IMAGE': tag,
Sree Kuchibhotlae3717422016-01-04 10:14:38 -0800201 'BASE_NAME': 'grpc_interop_stress_%s' % language.safename}
Jan Tattermusch9835d4b2016-04-29 15:05:05 -0700202 build_job = jobset.JobSpec(cmdline=['tools/run_tests/dockerize/build_interop_stress_image.sh'],
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800203 environ=env,
204 shortname='build_docker_%s' % (language),
205 timeout_seconds=30 * 60)
206 build_job.tag = tag
207 return build_job
208
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800209argp = argparse.ArgumentParser(description='Run stress tests.')
210argp.add_argument('-l',
211 '--language',
212 choices=['all'] + sorted(_LANGUAGES),
213 nargs='+',
214 default=['all'],
215 help='Clients to run.')
216argp.add_argument('-j', '--jobs', default=multiprocessing.cpu_count(), type=int)
217argp.add_argument(
218 '-s',
219 '--server',
220 choices=['all'] + sorted(_SERVERS),
221 action='append',
222 help='Run cloud_to_cloud servers in a separate docker ' + 'image.',
223 default=[])
224argp.add_argument(
225 '--override_server',
226 action='append',
227 type=lambda kv: kv.split('='),
228 help=
229 'Use servername=HOST:PORT to explicitly specify a server. E.g. '
230 'csharp=localhost:50000',
231 default=[])
232argp.add_argument('--test_duration_secs',
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800233 help='The duration of the test in seconds',
Sree Kuchibhotlaca8e3d72016-01-04 09:39:05 -0800234 default=_DEFAULT_TEST_DURATION_SECS)
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800235
236args = argp.parse_args()
237
238servers = set(
239 s
240 for s in itertools.chain.from_iterable(_SERVERS if x == 'all' else [x]
241 for x in args.server))
242
Siddharth Shuklad194f592017-03-11 19:12:43 +0100243languages = set(_LANGUAGES[l] for l in itertools.chain.from_iterable(
244 six.iterkeys(_LANGUAGES) if x == 'all' else [x] for x in args.language))
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800245
246docker_images = {}
247# languages for which to build docker images
248languages_to_build = set(
249 _LANGUAGES[k]
250 for k in set([str(l) for l in languages] + [s for s in servers]))
251build_jobs = []
252for l in languages_to_build:
Sree Kuchibhotlae3717422016-01-04 10:14:38 -0800253 job = build_interop_stress_image_jobspec(l)
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800254 docker_images[str(l)] = job.tag
255 build_jobs.append(job)
256
257if build_jobs:
258 jobset.message('START', 'Building interop docker images.', do_newline=True)
259 num_failures, _ = jobset.run(build_jobs,
260 newline_on_success=True,
261 maxjobs=args.jobs)
262 if num_failures == 0:
263 jobset.message('SUCCESS',
264 'All docker images built successfully.',
265 do_newline=True)
266 else:
267 jobset.message('FAILED',
268 'Failed to build interop docker images.',
269 do_newline=True)
Siddharth Shuklad194f592017-03-11 19:12:43 +0100270 for image in six.itervalues(docker_images):
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800271 dockerjob.remove_image(image, skip_nonexistent=True)
272 sys.exit(1)
273
274# Start interop servers.
275server_jobs = {}
276server_addresses = {}
277try:
278 for s in servers:
279 lang = str(s)
Sree Kuchibhotlaca8e3d72016-01-04 09:39:05 -0800280 spec = server_jobspec(_LANGUAGES[lang], docker_images.get(lang), args.test_duration_secs)
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800281 job = dockerjob.DockerJob(spec)
282 server_jobs[lang] = job
283 server_addresses[lang] = ('localhost',
284 job.mapped_port(_DEFAULT_SERVER_PORT))
285
286 jobs = []
287
288 for server in args.override_server:
289 server_name = server[0]
290 (server_host, server_port) = server[1].split(':')
291 server_addresses[server_name] = (server_host, server_port)
292
siddharthshukla0589e532016-07-07 16:08:01 +0200293 for server_name, server_address in server_addresses.items():
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800294 (server_host, server_port) = server_address
295 for language in languages:
296 test_job = cloud_to_cloud_jobspec(
297 language,
298 _DEFAULT_TEST_CASES,
299 ('%s:%s' % (server_host, server_port)),
Sree Kuchibhotlaca8e3d72016-01-04 09:39:05 -0800300 args.test_duration_secs,
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800301 _DEFAULT_NUM_CHANNELS_PER_SERVER,
302 _DEFAULT_NUM_STUBS_PER_CHANNEL,
303 _DEFAULT_METRICS_PORT,
304 docker_image=docker_images.get(str(language)))
305 jobs.append(test_job)
306
307 if not jobs:
siddharthshukla0589e532016-07-07 16:08:01 +0200308 print('No jobs to run.')
Siddharth Shuklad194f592017-03-11 19:12:43 +0100309 for image in six.itervalues(docker_images):
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800310 dockerjob.remove_image(image, skip_nonexistent=True)
311 sys.exit(1)
312
313 num_failures, resultset = jobset.run(jobs,
314 newline_on_success=True,
315 maxjobs=args.jobs)
316 if num_failures:
317 jobset.message('FAILED', 'Some tests failed', do_newline=True)
318 else:
319 jobset.message('SUCCESS', 'All tests passed', do_newline=True)
320
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800321finally:
322 # Check if servers are still running.
siddharthshukla0589e532016-07-07 16:08:01 +0200323 for server, job in server_jobs.items():
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800324 if not job.is_running():
siddharthshukla0589e532016-07-07 16:08:01 +0200325 print('Server "%s" has exited prematurely.' % server)
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800326
Siddharth Shuklad194f592017-03-11 19:12:43 +0100327 dockerjob.finish_jobs([j for j in six.itervalues(server_jobs)])
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800328
Siddharth Shuklad194f592017-03-11 19:12:43 +0100329 for image in six.itervalues(docker_images):
siddharthshukla0589e532016-07-07 16:08:01 +0200330 print('Removing docker image %s' % image)
Sree Kuchibhotla1b38bb42015-12-14 17:22:38 -0800331 dockerjob.remove_image(image)