def handle_function_service_expiration(ctx, engine): """Clean up resources related to expired functions. If it's image function, we will rely on the orchestrator itself to do the image clean up, e.g. image collection feature in kubernetes. """ context.set_ctx(ctx) delta = timedelta(seconds=CONF.engine.function_service_expiration) expiry_time = datetime.utcnow() - delta results = db_api.get_functions(sort_keys=['updated_at'], insecure=True, updated_at={'lte': expiry_time}) if len(results) == 0: return for func_db in results: if not etcd_util.get_service_url(func_db.id): continue LOG.info('Deleting service mapping and workers for function %s', func_db.id) # Delete resources related to the function engine.delete_function(ctx, func_db.id) # Delete etcd keys etcd_util.delete_function(func_db.id)
def test_update_recurring_job_failed(self): job_id = self.create_job( self.function_id, first_execution_time=datetime.utcnow() + timedelta(hours=1), next_execution_time=datetime.utcnow() + timedelta(hours=1), pattern='0 */1 * * *', status=status.RUNNING, count=10).id url = '/v1/jobs/%s' % job_id # Try to change job type resp = self.app.put_json(url, {'pattern': ''}, expect_errors=True) self.assertEqual(400, resp.status_int) self.assertIn('Can not change job type.', resp.json['faultstring']) # Pause the job and try to resume with an invalid next_execution_time auth_context.set_ctx(self.ctx) self.addCleanup(auth_context.set_ctx, None) db_api.update_job(job_id, {'status': status.PAUSED}) resp = self.app.put_json( url, { 'status': status.RUNNING, 'next_execution_time': str(datetime.utcnow() - timedelta(hours=1)) }, expect_errors=True) self.assertEqual(400, resp.status_int) self.assertIn('Execution time must be at least 1 minute in the future', resp.json['faultstring'])
def handle_function_service_expiration(ctx, engine_client, orchestrator): context.set_ctx(ctx) delta = timedelta(seconds=CONF.engine.function_service_expiration) expiry_time = datetime.utcnow() - delta results = db_api.get_functions(sort_keys=['updated_at'], insecure=True, updated_at={'lte': expiry_time}) if len(results) == 0: return for func_db in results: if not func_db.service: continue with db_api.transaction(): LOG.info('Deleting service mapping and workers for function %s', func_db.id) # Delete resources related to the function engine_client.delete_function(func_db.id) # Delete service mapping and worker records db_api.delete_function_service_mapping(func_db.id) db_api.delete_function_workers(func_db.id)
def test_invoke_with_function_alias(self, mock_create_execution, mock_create_context): exec_mock = mock_create_execution.return_value exec_mock.id = "fake_id" db_api.increase_function_version(self.func_id, 0) alias_name = self.rand_name(name="alias", prefix=self.prefix) body = { 'function_id': self.func_id, 'function_version': 1, 'name': alias_name } db_api.create_function_alias(**body) webhook = self.create_webhook(function_alias=alias_name) resp = self.app.post_json('/v1/webhooks/%s/invoke' % webhook.id, {}) context.set_ctx(self.ctx) self.assertEqual(202, resp.status_int) params = { 'function_id': self.func_id, 'function_version': 1, 'sync': False, 'input': json.dumps({}), 'description': constants.EXECUTION_BY_WEBHOOK % webhook.id } mock_create_execution.assert_called_once_with(mock.ANY, params)
def test_update_one_shot_job_failed(self): job_id = self.create_job(self.function_id, prefix='TestJobController', first_execution_time=datetime.utcnow(), next_execution_time=datetime.utcnow() + timedelta(hours=1), status=status.RUNNING, count=1).id url = '/v1/jobs/%s' % job_id # Try to change job type resp = self.app.put_json(url, {'pattern': '*/1 * * * *'}, expect_errors=True) self.assertEqual(400, resp.status_int) self.assertIn('Can not change job type.', resp.json['faultstring']) # Try to resume job but the execution time is invalid auth_context.set_ctx(self.ctx) self.addCleanup(auth_context.set_ctx, None) db_api.update_job( job_id, { 'next_execution_time': datetime.utcnow() - timedelta(hours=1), 'status': status.PAUSED }) resp = self.app.put_json(url, {'status': status.RUNNING}, expect_errors=True) self.assertEqual(400, resp.status_int) self.assertIn('Execution time must be at least 1 minute in the future', resp.json['faultstring'])
def invoke(self, id, **kwargs): with db_api.transaction(): # The webhook url can be accessed without authentication, so # insecure is used here webhook_db = db_api.get_webhook(id, insecure=True) function_db = webhook_db.function trust_id = function_db.trust_id project_id = function_db.project_id version = webhook_db.function_version LOG.info( 'Invoking function %s(version %s) by webhook %s', webhook_db.function_id, version, id ) # Setup user context ctx = keystone_utils.create_trust_context(trust_id, project_id) context.set_ctx(ctx) params = { 'function_id': webhook_db.function_id, 'function_version': version, 'sync': False, 'input': json.dumps(kwargs), 'description': constants.EXECUTION_BY_WEBHOOK % id } execution = executions.create_execution(self.engine_client, params) pecan.response.status = 202 return {'execution_id': execution.id}
def setUp(self): super(DbTestCase, self).setUp() self._heavy_init() self.ctx = get_context() auth_context.set_ctx(self.ctx) self.addCleanup(auth_context.set_ctx, None) self.addCleanup(self._clean_db)
def test_update_recurring_job(self): job_id = self.create_job( self.function_id, first_execution_time=datetime.utcnow() + timedelta(hours=1), next_execution_time=datetime.utcnow() + timedelta(hours=1), pattern='0 */1 * * *', status=status.RUNNING, count=10).id next_hour_and_half = datetime.utcnow() + timedelta(hours=1.5) next_two_hours = datetime.utcnow() + timedelta(hours=2) req_body = { 'next_execution_time': str(next_hour_and_half.strftime('%Y-%m-%dT%H:%M:%SZ')), 'pattern': '1 */1 * * *' } resp = self.app.put_json('/v1/jobs/%s' % job_id, req_body) self.assertEqual(200, resp.status_int) self._assertDictContainsSubset(resp.json, req_body) # Pause the job and resume with a valid next_execution_time req_body = {'status': status.PAUSED} resp = self.app.put_json('/v1/jobs/%s' % job_id, req_body) self.assertEqual(200, resp.status_int) self._assertDictContainsSubset(resp.json, req_body) req_body = { 'status': status.RUNNING, 'next_execution_time': str(next_two_hours.strftime('%Y-%m-%dT%H:%M:%SZ')), } resp = self.app.put_json('/v1/jobs/%s' % job_id, req_body) self.assertEqual(200, resp.status_int) self._assertDictContainsSubset(resp.json, req_body) # Pause the job and resume without specifying next_execution_time auth_context.set_ctx(self.ctx) self.addCleanup(auth_context.set_ctx, None) db_api.update_job( job_id, { 'next_execution_time': datetime.utcnow() - timedelta(hours=1), 'status': status.PAUSED }) req_body = {'status': status.RUNNING} resp = self.app.put_json('/v1/jobs/%s' % job_id, req_body) self.assertEqual(200, resp.status_int) self._assertDictContainsSubset(resp.json, req_body)
def test_job_handler_with_version(self, mock_next_time): db_func = self.create_function() function_id = db_func.id new_version = db_api.increase_function_version(function_id, 0) self.assertEqual(0, new_version.count) now = datetime.utcnow() db_job = self.create_job( function_id, function_version=1, status=status.RUNNING, next_execution_time=now, count=2 ) job_id = db_job.id e_client = mock.Mock() # It doesn't matter what's the returned value, but need to be in # datetime type. mock_next_time.return_value = now + timedelta(seconds=1) periodics.handle_job(e_client) context.set_ctx(self.ctx) db_job = db_api.get_job(job_id) self.assertEqual(1, db_job.count) db_func = db_api.get_function(function_id) self.assertEqual(0, db_func.count) db_version = db_api.get_function_version(function_id, 1) self.assertEqual(1, db_version.count) db_execs = db_api.get_executions(function_id=function_id, function_version=1) self.assertEqual(1, len(db_execs)) periodics.handle_job(e_client) context.set_ctx(self.ctx) db_job = db_api.get_job(job_id) self.assertEqual(0, db_job.count) self.assertEqual(status.DONE, db_job.status) db_func = db_api.get_function(function_id) self.assertEqual(0, db_func.count) db_version = db_api.get_function_version(function_id, 1) self.assertEqual(2, db_version.count) db_execs = db_api.get_executions(function_id=function_id, function_version=1) self.assertEqual(2, len(db_execs))
def test_get(self): name = 'TestAlias' function_version = 0 body = { 'function_id': self.func_id, 'function_version': function_version, 'name': name, 'description': 'new alias' } db_api.create_function_alias(**body) resp = self.app.get('/v1/aliases/%s' % name) context.set_ctx(self.ctx) self.assertEqual(200, resp.status_int) self.assertEqual("new alias", resp.json.get('description'))
def test_post(self): name = 'TestAlias' body = { 'function_id': self.func_id, 'name': name, 'description': 'new alias' } resp = self.app.post_json('/v1/aliases', body) self.assertEqual(201, resp.status_int) self._assertDictContainsSubset(resp.json, body) context.set_ctx(self.ctx) func_alias_db = db_api.get_function_alias(name) self.assertEqual(name, func_alias_db.name) self.assertEqual(0, func_alias_db.function_version)
def test_invoke_with_function_id(self, mock_create_execution, mock_create_context): exec_mock = mock_create_execution.return_value exec_mock.id = "fake_id" webhook = self.create_webhook(function_id=self.func_id) resp = self.app.post_json('/v1/webhooks/%s/invoke' % webhook.id, {}) context.set_ctx(self.ctx) self.assertEqual(202, resp.status_int) params = { 'function_id': self.func_id, 'function_version': None, 'sync': False, 'input': json.dumps({}), 'description': constants.EXECUTION_BY_WEBHOOK % webhook.id } mock_create_execution.assert_called_once_with(mock.ANY, params)
def test_delete(self): name = self.rand_name(name="alias", prefix=self.prefix) function_version = 0 body = { 'function_id': self.func_id, 'function_version': function_version, 'name': name, 'description': 'new alias' } db_api.create_function_alias(**body) resp = self.app.delete('/v1/aliases/%s' % name) self.assertEqual(204, resp.status_int) context.set_ctx(self.ctx) self.assertRaises(exc.DBEntityNotFoundError, db_api.get_function_alias, name)
def test_delete(self, mock_package_delete, mock_engine_delete, mock_etcd_delete): db_api.increase_function_version(self.func_id, 0, description="version 1") resp = self.app.delete('/v1/functions/%s/versions/1' % self.func_id) self.assertEqual(204, resp.status_int) mock_engine_delete.assert_called_once_with(self.func_id, version=1) mock_etcd_delete.assert_called_once_with(self.func_id, version=1) mock_package_delete.assert_called_once_with( unit_base.DEFAULT_PROJECT_ID, self.func_id, None, version=1) # We need to set context as it was removed after the API call context.set_ctx(self.ctx) with db_api.transaction(): func_db = db_api.get_function(self.func_id) self.assertEqual(0, len(func_db.versions)) self.assertEqual(0, func_db.latest_version)
def test_job_handler(self, mock_get_next): db_func = self.create_function() function_id = db_func.id self.assertEqual(0, db_func.count) now = datetime.utcnow() db_job = self.create_job( function_id, status=status.RUNNING, next_execution_time=now, count=2 ) job_id = db_job.id e_client = mock.Mock() mock_get_next.return_value = now + timedelta(seconds=1) periodics.handle_job(e_client) context.set_ctx(self.ctx) db_job = db_api.get_job(job_id) self.assertEqual(1, db_job.count) db_func = db_api.get_function(function_id) self.assertEqual(1, db_func.count) db_execs = db_api.get_executions(function_id=function_id) self.assertEqual(1, len(db_execs)) periodics.handle_job(e_client) context.set_ctx(self.ctx) db_job = db_api.get_job(job_id) self.assertEqual(0, db_job.count) self.assertEqual(status.DONE, db_job.status) db_func = db_api.get_function(function_id) self.assertEqual(2, db_func.count) db_execs = db_api.get_executions(function_id=function_id) self.assertEqual(2, len(db_execs))
def handle_job(engine_client): """Execute job task with no db transactions.""" for job in db_api.get_next_jobs(timeutils.utcnow() + timedelta(seconds=3)): job_id = job.id func_id = job.function_id LOG.debug("Processing job: %s, function: %s", job_id, func_id) func_db = db_api.get_function(func_id, insecure=True) trust_id = func_db.trust_id try: # Setup context before schedule job. ctx = keystone_utils.create_trust_context(trust_id, job.project_id) context.set_ctx(ctx) if (job.count is not None and job.count > 0): job.count -= 1 # Job delete/update is done using UPDATE ... FROM ... WHERE # non-locking clause. if job.count == 0: modified = db_api.conditional_update( models.Job, { 'status': status.DONE, 'count': 0 }, { 'id': job_id, 'status': status.RUNNING }, insecure=True, ) else: next_time = jobs.get_next_execution_time( job.pattern, job.next_execution_time) modified = db_api.conditional_update( models.Job, { 'next_execution_time': next_time, 'count': job.count }, { 'id': job_id, 'next_execution_time': job.next_execution_time }, insecure=True, ) if not modified: LOG.warning( 'Job %s has been already handled by another periodic ' 'task.', job_id) continue LOG.debug("Starting to execute function %s by job %s", func_id, job_id) params = { 'function_id': func_id, 'input': job.function_input, 'sync': False, 'description': constants.EXECUTION_BY_JOB % job_id } executions.create_execution(engine_client, params) except Exception: LOG.exception("Failed to process job %s", job_id) finally: context.set_ctx(None)
def deserialize_context(self, context): qinling_ctx = ctx.Context.from_dict(context) ctx.set_ctx(qinling_ctx) return qinling_ctx