From 731f187168b7ebd1b33e7b7038dd5ba8c542dce4 Mon Sep 17 00:00:00 2001 From: Krzysztof Date: Tue, 25 Aug 2015 12:40:17 +0200 Subject: [PATCH] Google Cloud Autoscaler demo This application is a very simple web server, which responds to get requests by consuming some CPU cycles. It is part of a demo of the Google Compute Engine Autoscaler. Link to the Autoscaler tutorial video which refers to this application will follow. --- compute/__init__.py | 0 compute/autoscaler/__init__.py | 0 compute/autoscaler/demo/__init__.py | 0 compute/autoscaler/demo/frontend.py | 107 ++++++++++++++++++ compute/autoscaler/demo/tests/__init__.py | 0 .../autoscaler/demo/tests/test_frontend.py | 70 ++++++++++++ 6 files changed, 177 insertions(+) create mode 100644 compute/__init__.py create mode 100644 compute/autoscaler/__init__.py create mode 100644 compute/autoscaler/demo/__init__.py create mode 100644 compute/autoscaler/demo/frontend.py create mode 100644 compute/autoscaler/demo/tests/__init__.py create mode 100644 compute/autoscaler/demo/tests/test_frontend.py diff --git a/compute/__init__.py b/compute/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/compute/autoscaler/__init__.py b/compute/autoscaler/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/compute/autoscaler/demo/__init__.py b/compute/autoscaler/demo/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/compute/autoscaler/demo/frontend.py b/compute/autoscaler/demo/frontend.py new file mode 100644 index 000000000000..f985ad404601 --- /dev/null +++ b/compute/autoscaler/demo/frontend.py @@ -0,0 +1,107 @@ +# Copyright 2015, Google, Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""A simple web server which responds to HTTP GET requests by consuming CPU. +This binary runs in a GCE VM. It serves HTTP requests on port 80. Every request +with path '/service' consumes 1 core-second of CPU time, with the timeout of +5 (walltime) seconds. The purpose of this application is to demonstrate how +Google Compute Engine Autoscaler can scale a web frontend server based on CPU +utilization. +The original version of this file is available here: +https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/compute/ + autoscaler/demo/tests/test_frontend.py +""" + +import BaseHTTPServer +from multiprocessing import Process +import os +import SocketServer +import sys +import time + +REQUEST_CPUTIME_SEC = 1.0 +REQUEST_TIMEOUT_SEC = 5.0 + + +class CpuBurner(object): + def get_walltime(self): + return time.time() + + def get_user_cputime(self): + return os.times()[0] + + def busy_wait(self): + for _ in xrange(100000): + pass + + def burn_cpu(self): + """Consume REQUEST_CPUTIME_SEC core seconds. + This method consumes REQUEST_CPUTIME_SEC core seconds. If unable to + complete within REQUEST_TIMEOUT_SEC walltime seconds, it times out and + terminates the process. + """ + start_walltime_sec = self.get_walltime() + start_cputime_sec = self.get_user_cputime() + while (self.get_user_cputime() < + start_cputime_sec + REQUEST_CPUTIME_SEC): + self.busy_wait() + if (self.get_walltime() > + start_walltime_sec + REQUEST_TIMEOUT_SEC): + sys.exit(1) + + def handle_http_request(self): + """Process a request to consume CPU and produce an HTTP response.""" + start_time = self.get_walltime() + p = Process(target=self.burn_cpu) # Run in a separate process. + p.start() + # Force kill after timeout + 1 sec. + p.join(timeout=REQUEST_TIMEOUT_SEC + 1) + if p.is_alive(): + p.terminate() + if p.exitcode != 0: + return (500, "Request failed\n") + else: + end_time = self.get_walltime() + response = "Request took %.2f walltime seconds\n" % ( + end_time - start_time) + return (200, response) + + +class DemoRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + """Request handler for Demo http server.""" + + def do_GET(self): + """Handle an HTTP GET request.""" + mapping = { + "/": lambda: (200, "OK"), # Return HTTP 200 response. + "/service": CpuBurner().handle_http_request, + } + if self.path not in mapping: + self.send_response(404) + self.end_headers() + return + (code, response) = mapping[self.path]() + self.send_response(code) + self.end_headers() + self.wfile.write(response) + self.wfile.close() + + +class DemoHttpServer(SocketServer.ThreadingMixIn, + BaseHTTPServer.HTTPServer): + pass + + +if __name__ == "__main__": + httpd = DemoHttpServer(("", 80), DemoRequestHandler) + httpd.serve_forever() diff --git a/compute/autoscaler/demo/tests/__init__.py b/compute/autoscaler/demo/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/compute/autoscaler/demo/tests/test_frontend.py b/compute/autoscaler/demo/tests/test_frontend.py new file mode 100644 index 000000000000..702371500b1d --- /dev/null +++ b/compute/autoscaler/demo/tests/test_frontend.py @@ -0,0 +1,70 @@ +# Copyright 2015, Google, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import unittest + +from compute.autoscaler.demo import frontend + + +class FakeTime(object): + """Fake implementations of GetUserCpuTime, GetUserCpuTime and BusyWait. + Each call to BusyWait advances both the cpu and the wall clocks by fixed + intervals (cpu_time_step and wall_time_step, respectively). This can be + used to simulate arbitrary fraction of CPU time available to the process. + """ + def __init__(self, cpu_time_step=1.0, wall_time_step=1.0): + self.cpu_time = 0.0 + self.wall_time = 0.0 + self.cpu_time_step = cpu_time_step + self.wall_time_step = wall_time_step + + def get_walltime(self): + return self.wall_time + + def get_user_cputime(self): + return self.cpu_time + + def busy_wait(self): + self.wall_time += self.wall_time_step + self.cpu_time += self.cpu_time_step + + +class TestHandlers(unittest.TestCase): + def setUp(self): + self.fake_time = FakeTime() + self.cpu_burner = frontend.CpuBurner() + self.cpu_burner.get_user_cputime = self.fake_time.get_user_cputime + self.cpu_burner.get_walltime = self.fake_time.get_walltime + self.cpu_burner.busy_wait = self.fake_time.busy_wait + + # In this test scenario CPU time advances at 25% of the wall time speed. + # Given the request requires 1 CPU core second, we expect it to finish + # within the timeout (5 seconds) and return success. + def test_ok_response(self): + self.fake_time.cpu_time_step = 0.25 + (code, _) = self.cpu_burner.handle_http_request() + self.assertEqual(200, code) + + # In this test scenario CPU time advances at 15% of the wall time speed. + # Given the request requires 1 CPU core second, we expect it to timeout + # after 5 simulated wall time seconds and return error 500. + def test_timeout(self): + self.fake_time.cpu_time_step = 0.15 + (code, _) = self.cpu_burner.handle_http_request() + self.assertEqual(500, code) + + +if __name__ == '__main__': + unittest.main()