From a8425dc7cfc4c25226debb554e124f335eba1bed Mon Sep 17 00:00:00 2001 From: Hunt Xu Date: Fri, 8 Jun 2018 19:13:34 +0800 Subject: [PATCH] Add simple tempest testcases for resource limiting Change-Id: I928b80e83f3101f1a8dd0564a8fad4381deb3fef Story: 2001586 Task: 20065 --- .../functions/python/test_python_cpu_limit.py | 40 ++++++ .../python/test_python_memory_limit.py | 23 ++++ .../services/qinling_client.py | 3 +- .../tests/api/test_executions.py | 122 ++++++++++++++++++ 4 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 qinling_tempest_plugin/functions/python/test_python_cpu_limit.py create mode 100644 qinling_tempest_plugin/functions/python/test_python_memory_limit.py diff --git a/qinling_tempest_plugin/functions/python/test_python_cpu_limit.py b/qinling_tempest_plugin/functions/python/test_python_cpu_limit.py new file mode 100644 index 00000000..1537bccf --- /dev/null +++ b/qinling_tempest_plugin/functions/python/test_python_cpu_limit.py @@ -0,0 +1,40 @@ +# Copyright 2018 AWCloud Software Co., Ltd +# +# 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. + + +# Codes are from https://www.craig-wood.com/nick/articles/pi-machin/ + +def arctan_euler(x, one): + x_squared = x * x + x_squared_plus_1 = x_squared + 1 + term = (x * one) // x_squared_plus_1 + total = term + two_n = 2 + while True: + divisor = (two_n + 1) * x_squared_plus_1 + term *= two_n + term = term // divisor + if term == 0: + break + total += term + two_n += 2 + return total + + +def pi_machin(one): + return 4 * (4 * arctan_euler(5, one) - arctan_euler(239, one)) + + +def main(digit=50000, *args, **kwargs): + return str(pi_machin(10**digit))[:15] diff --git a/qinling_tempest_plugin/functions/python/test_python_memory_limit.py b/qinling_tempest_plugin/functions/python/test_python_memory_limit.py new file mode 100644 index 00000000..4d7f28d6 --- /dev/null +++ b/qinling_tempest_plugin/functions/python/test_python_memory_limit.py @@ -0,0 +1,23 @@ +# Copyright 2018 AWCloud Software Co., Ltd +# +# 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. + + +S = 20 * 1024 * 1024 # 20M + + +def main(*args, **kwargs): + L = [] + for i in 'abcd': + L.append(i * S) + return len(L) diff --git a/qinling_tempest_plugin/services/qinling_client.py b/qinling_tempest_plugin/services/qinling_client.py index 4d1804c9..d5641b17 100644 --- a/qinling_tempest_plugin/services/qinling_client.py +++ b/qinling_tempest_plugin/services/qinling_client.py @@ -85,7 +85,7 @@ class QinlingClient(client_base.QinlingClientBase): return resp, json.loads(resp.text) def update_function(self, function_id, package_data=None, code=None, - entry=None): + entry=None, **kwargs): headers = {'X-Auth-Token': self.auth_provider.get_token()} req_body = {} @@ -93,6 +93,7 @@ class QinlingClient(client_base.QinlingClientBase): req_body['code'] = json.dumps(code) if entry: req_body['entry'] = entry + req_body.update(kwargs) req_kwargs = { 'headers': headers, diff --git a/qinling_tempest_plugin/tests/api/test_executions.py b/qinling_tempest_plugin/tests/api/test_executions.py index 377b14e2..2f61ca94 100644 --- a/qinling_tempest_plugin/tests/api/test_executions.py +++ b/qinling_tempest_plugin/tests/api/test_executions.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. from concurrent import futures +import json import futurist from oslo_serialization import jsonutils @@ -20,6 +21,9 @@ from tempest.lib import exceptions from qinling_tempest_plugin.tests import base +INVOKE_ERROR = "Function execution failed because of too much resource " \ + "consumption" + class ExecutionsTest(base.BaseQinlingTest): name_prefix = 'ExecutionsTest' @@ -273,3 +277,121 @@ class ExecutionsTest(base.BaseQinlingTest): execution_id, ignore_notfound=True) self.assertEqual('success', body['status']) self.assertIn('Qinling', jsonutils.loads(body['result'])['output']) + + @decorators.idempotent_id('2b5f0787-b82d-4fc4-af76-cf86d389a76b') + def test_python_execution_memory_limit_non_image(self): + """In this case, the following steps are taken: + + 1. Create a function that requires ~80M memory to run. + 2. Create an execution using the function. + 3. Verify that the execution is killed by the OOM-killer + because the function memory limit is only 32M(default). + 4. Increase the function memory limit to 96M. + 5. Create another execution. + 6. Check the execution finished normally. + """ + + # Create function + package = self.create_package( + name='python/test_python_memory_limit.py' + ) + function_id = self.create_function(package_path=package) + + # Invoke function + resp, body = self.client.create_execution(function_id) + + execution_id = body['id'] + self.addCleanup(self.client.delete_resource, 'executions', + execution_id, ignore_notfound=True) + + # Check the process is killed + self.assertEqual(201, resp.status) + result = json.loads(body['result']) + output = result.get('output') + self.assertEqual(INVOKE_ERROR, output) + + # Increase the memory limit to 100663296(96M). + resp, body = self.client.update_function( + function_id, memory_size=100663296) + self.assertEqual(200, resp.status_code) + + # Invoke the function again + resp, body = self.client.create_execution(function_id) + + execution_id = body['id'] + self.addCleanup(self.client.delete_resource, 'executions', + execution_id, ignore_notfound=True) + + # Check the process exited normally + self.assertEqual(201, resp.status) + result = json.loads(body['result']) + output = result.get('output') + # The function returns the length of a list containing 4 long strings. + self.assertEqual(4, output) + + @decorators.idempotent_id('ed714f98-29fe-4e8d-b6ee-9730f92bddea') + def test_python_execution_cpu_limit_non_image(self): + """In this case, the following steps are taken: + + 1. Create a function that takes some time to finish (calculating the + first 50000 digits of PI) + 2. Create an execution using the function. + 3. Store the duration of the first execution. + 4. Increase the function cpu limit from 100(default) to 200 millicpu. + 5. Create another execution. + 6. Check whether the duration of the first execution is approximately + the double of the duration of the second one as its cpu resource is + half of the second run. + """ + + # Create function + package = self.create_package( + name='python/test_python_cpu_limit.py' + ) + function_id = self.create_function(package_path=package) + + # Invoke function + resp, body = self.client.create_execution(function_id) + + execution_id = body['id'] + self.addCleanup(self.client.delete_resource, 'executions', + execution_id, ignore_notfound=True) + + # Record the duration, check whether the result is correct. + self.assertEqual(201, resp.status) + result = json.loads(body['result']) + output = result.get('output') + # Only the first 15 digits are returned. + self.assertEqual('314159265358979', output) + first_duration = result.get('duration', 0) + + # Increase the cpu limit + resp, body = self.client.update_function(function_id, cpu=200) + self.assertEqual(200, resp.status_code) + + # Invoke the function again + resp, body = self.client.create_execution(function_id) + + execution_id = body['id'] + self.addCleanup(self.client.delete_resource, 'executions', + execution_id, ignore_notfound=True) + + # Record the second duration, check whether the result is correct. + self.assertEqual(201, resp.status) + result = json.loads(body['result']) + output = result.get('output') + # Only the first 15 digits are returned. + self.assertEqual('314159265358979', output) + second_duration = result.get('duration', 0) + + # Check whether the duration of the first execution is approximately + # the double (1.8x ~ 2.2x) of the duration of the second one. + # NOTE(huntxu): on my testbed, the result is quite near 2x. However + # it may vary in different environments, so we give a wider range + # here. + self.assertNotEqual(0, first_duration) + self.assertNotEqual(0, second_duration) + upper = second_duration * 2.2 + lower = second_duration * 1.8 + self.assertGreaterEqual(upper, first_duration) + self.assertLessEqual(lower, first_duration)