diff --git a/qinling/api/controllers/v1/function.py b/qinling/api/controllers/v1/function.py index 88d769ae..33220425 100644 --- a/qinling/api/controllers/v1/function.py +++ b/qinling/api/controllers/v1/function.py @@ -164,6 +164,9 @@ class FunctionsController(rest.RestController): 'memory_size': kwargs.get( 'memory_size', CONF.resource_limits.default_memory ), + 'timeout': kwargs.get( + 'timeout', CONF.resource_limits.default_timeout + ), } # Check cpu and memory_size values. diff --git a/qinling/config.py b/qinling/config.py index 584608c9..0fdd13ae 100644 --- a/qinling/config.py +++ b/qinling/config.py @@ -254,6 +254,11 @@ rlimits_opts = [ default=134217728, help='Maximum memory resource(unit: bytes).' ), + cfg.IntOpt( + 'default_timeout', + default=5, + help='Default function execution timeout(unit: seconds)' + ), ] diff --git a/qinling/engine/default_engine.py b/qinling/engine/default_engine.py index 303c1318..091f2c7f 100644 --- a/qinling/engine/default_engine.py +++ b/qinling/engine/default_engine.py @@ -172,7 +172,7 @@ class DefaultEngine(object): data = utils.get_request_data( CONF, function_id, function_version, execution_id, rlimit, input, function.entry, function.trust_id, - self.qinling_endpoint + self.qinling_endpoint, function.timeout ) success, res = utils.url_request( self.session, func_url, body=data diff --git a/qinling/engine/utils.py b/qinling/engine/utils.py index 1a6de7f4..c17eac0d 100644 --- a/qinling/engine/utils.py +++ b/qinling/engine/utils.py @@ -76,7 +76,7 @@ def url_request(request_session, url, body=None): def get_request_data(conf, function_id, version, execution_id, rlimit, input, - entry, trust_id, qinling_endpoint): + entry, trust_id, qinling_endpoint, timeout): """Prepare the request body should send to the worker.""" ctx = context.get_ctx() @@ -103,6 +103,7 @@ def get_request_data(conf, function_id, version, execution_id, rlimit, input, 'entry': entry, 'download_url': download_url, 'request_id': ctx.request_id, + 'timeout': timeout, } if conf.pecan.auth_enable: data.update( diff --git a/qinling/orchestrator/kubernetes/manager.py b/qinling/orchestrator/kubernetes/manager.py index e8ccff1e..56a1fdde 100644 --- a/qinling/orchestrator/kubernetes/manager.py +++ b/qinling/orchestrator/kubernetes/manager.py @@ -484,7 +484,7 @@ class KubernetesManager(base.OrchestratorBase): def run_execution(self, execution_id, function_id, version, rlimit=None, input=None, identifier=None, service_url=None, - entry='main.main', trust_id=None): + entry='main.main', trust_id=None, timeout=None): """Run execution. Return a tuple including the result and the output. @@ -493,7 +493,7 @@ class KubernetesManager(base.OrchestratorBase): func_url = '%s/execute' % service_url data = utils.get_request_data( self.conf, function_id, version, execution_id, rlimit, input, - entry, trust_id, self.qinling_endpoint + entry, trust_id, self.qinling_endpoint, timeout ) LOG.debug( 'Invoke function %s(version %s), url: %s, data: %s', diff --git a/qinling/tests/unit/api/controllers/v1/test_function.py b/qinling/tests/unit/api/controllers/v1/test_function.py index da43a696..5675641d 100644 --- a/qinling/tests/unit/api/controllers/v1/test_function.py +++ b/qinling/tests/unit/api/controllers/v1/test_function.py @@ -63,6 +63,25 @@ class TestFunctionController(base.APITest): ) self._assertDictContainsSubset(resp.json, body) + @mock.patch('qinling.storage.file_system.FileSystemStorage.store') + def test_post_timeout(self, mock_store): + mock_store.return_value = (True, 'fake_md5') + + with tempfile.NamedTemporaryFile() as f: + body = { + 'runtime_id': self.runtime_id, + 'code': json.dumps({"source": "package"}), + 'timeout': 3 + } + resp = self.app.post( + '/v1/functions', + params=body, + upload_files=[('package', f.name, f.read())] + ) + + self.assertEqual(201, resp.status_int) + self.assertEqual(3, resp.json['timeout']) + @mock.patch("qinling.utils.openstack.keystone.create_trust") @mock.patch('qinling.utils.openstack.keystone.get_swiftclient') @mock.patch('qinling.context.AuthHook.before') @@ -160,6 +179,7 @@ class TestFunctionController(base.APITest): "project_id": unit_base.DEFAULT_PROJECT_ID, "cpu": cfg.CONF.resource_limits.default_cpu, "memory_size": cfg.CONF.resource_limits.default_memory, + "timeout": cfg.CONF.resource_limits.default_timeout, } resp = self.app.get('/v1/functions/%s' % db_func.id) @@ -176,6 +196,7 @@ class TestFunctionController(base.APITest): "project_id": unit_base.DEFAULT_PROJECT_ID, "cpu": cfg.CONF.resource_limits.default_cpu, "memory_size": cfg.CONF.resource_limits.default_memory, + "timeout": cfg.CONF.resource_limits.default_timeout, } resp = self.app.get('/v1/functions') diff --git a/qinling/tests/unit/base.py b/qinling/tests/unit/base.py index 6ac445e2..b8a6ba13 100644 --- a/qinling/tests/unit/base.py +++ b/qinling/tests/unit/base.py @@ -197,6 +197,7 @@ class DbTestCase(BaseTest): 'project_id': DEFAULT_PROJECT_ID, 'cpu': cfg.CONF.resource_limits.default_cpu, 'memory_size': cfg.CONF.resource_limits.default_memory, + 'timeout': cfg.CONF.resource_limits.default_timeout, } ) diff --git a/qinling/tests/unit/engine/test_default_engine.py b/qinling/tests/unit/engine/test_default_engine.py index e3e484a8..20c3efe2 100644 --- a/qinling/tests/unit/engine/test_default_engine.py +++ b/qinling/tests/unit/engine/test_default_engine.py @@ -443,7 +443,7 @@ class TestDefaultEngine(base.DbTestCase): engine_utils_get_request_data_mock.assert_called_once_with( mock.ANY, function_id, 0, execution_id, self.rlimit, 'input', function.entry, function.trust_id, - self.qinling_endpoint) + self.qinling_endpoint, function.timeout) engine_utils_url_request_mock.assert_called_once_with( self.default_engine.session, 'svc_url/execute', body='data') diff --git a/qinling/tests/unit/orchestrator/kubernetes/test_manager.py b/qinling/tests/unit/orchestrator/kubernetes/test_manager.py index 0b2186dc..33d25d3e 100644 --- a/qinling/tests/unit/orchestrator/kubernetes/test_manager.py +++ b/qinling/tests/unit/orchestrator/kubernetes/test_manager.py @@ -638,10 +638,11 @@ class TestKubernetesManager(base.DbTestCase): mock_request.return_value = (True, 'fake output') execution_id = common.generate_unicode_uuid() function_id = common.generate_unicode_uuid() + timeout = 3 result, output = self.manager.run_execution( execution_id, function_id, 0, rlimit=self.rlimit, - service_url='FAKE_URL' + service_url='FAKE_URL', timeout=timeout ) download_url = ('http://127.0.0.1:7070/v1/functions/%s?download=true' @@ -656,6 +657,7 @@ class TestKubernetesManager(base.DbTestCase): 'entry': 'main.main', 'download_url': download_url, 'request_id': self.ctx.request_id, + 'timeout': timeout, } mock_request.assert_called_once_with( diff --git a/qinling_tempest_plugin/services/qinling_client.py b/qinling_tempest_plugin/services/qinling_client.py index 87f33d47..10289ce1 100644 --- a/qinling_tempest_plugin/services/qinling_client.py +++ b/qinling_tempest_plugin/services/qinling_client.py @@ -56,7 +56,7 @@ class QinlingClient(client_base.QinlingClientBase): return resp, body def create_function(self, code, runtime_id, name='', package_data=None, - entry=None): + entry=None, timeout=None): """Create function. Tempest rest client doesn't support multipart upload, so use requests @@ -67,7 +67,8 @@ class QinlingClient(client_base.QinlingClientBase): req_body = { 'name': name, 'runtime_id': runtime_id, - 'code': json.dumps(code) + 'code': json.dumps(code), + 'timeout': timeout } if entry: req_body['entry'] = entry diff --git a/qinling_tempest_plugin/tests/base.py b/qinling_tempest_plugin/tests/base.py index 7d14707f..5097410e 100644 --- a/qinling_tempest_plugin/tests/base.py +++ b/qinling_tempest_plugin/tests/base.py @@ -118,7 +118,8 @@ class BaseQinlingTest(test.BaseTestCase): self.addCleanup(os.remove, zip_file) return zip_file - def create_function(self, package_path=None, image=False, md5sum=None): + def create_function(self, package_path=None, image=False, + md5sum=None, timeout=None): function_name = data_utils.rand_name( 'function', prefix=self.name_prefix @@ -139,7 +140,8 @@ class BaseQinlingTest(test.BaseTestCase): self.runtime_id, name=function_name, package_data=package_data, - entry='%s.main' % module_name + entry='%s.main' % module_name, + timeout=timeout ) else: resp, body = self.client.create_function( diff --git a/releasenotes/notes/function-timeout-905dc4b064b73fd3.yaml b/releasenotes/notes/function-timeout-905dc4b064b73fd3.yaml new file mode 100644 index 00000000..6533190c --- /dev/null +++ b/releasenotes/notes/function-timeout-905dc4b064b73fd3.yaml @@ -0,0 +1,5 @@ +--- +features: + - Users can now specify a timeout value for their function to prevent the function from running + indefinitely. When the specified timeout is reached, Qinling will terminate the function + execution.