From 810d16cfb5ed06ad4fd8f573350b78e915768934 Mon Sep 17 00:00:00 2001 From: Lluis Gifre <lluis.gifre@cttc.es> Date: Thu, 19 May 2022 16:34:27 +0200 Subject: [PATCH] Service component: - Migrated to use new generic gRPC servicer - Migrated to use new generic Rest servicer - Migrated to use new settings framework - Migrated tests to use new generic servicers and mock's - Extracted common unitary test functionalities into separate code files - Minor code styling/formatting --- src/service/Config.py | 19 ----- src/service/client/ServiceClient.py | 10 ++- src/service/service/ServiceService.py | 70 +++-------------- .../service/ServiceServiceServicerImpl.py | 9 +-- src/service/service/__main__.py | 46 +++-------- src/service/tests/MockService_Dependencies.py | 49 ++++++++++++ src/service/tests/PrepareTestScenario.py | 68 ++++++++++++++++ src/service/tests/test_unitary.py | 77 +------------------ 8 files changed, 153 insertions(+), 195 deletions(-) create mode 100644 src/service/tests/MockService_Dependencies.py create mode 100644 src/service/tests/PrepareTestScenario.py diff --git a/src/service/Config.py b/src/service/Config.py index 5d551b023..70a332512 100644 --- a/src/service/Config.py +++ b/src/service/Config.py @@ -12,22 +12,3 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging - -# General settings -LOG_LEVEL = logging.WARNING - -# gRPC settings -GRPC_SERVICE_PORT = 3030 -GRPC_MAX_WORKERS = 10 -GRPC_GRACE_PERIOD = 60 - -# Prometheus settings -METRICS_PORT = 9192 - -# Dependency micro-service connection settings -CONTEXT_SERVICE_HOST = '127.0.0.1' -CONTEXT_SERVICE_PORT = 1010 - -DEVICE_SERVICE_HOST = '127.0.0.1' -DEVICE_SERVICE_PORT = 2020 diff --git a/src/service/client/ServiceClient.py b/src/service/client/ServiceClient.py index a44842768..a6335bfce 100644 --- a/src/service/client/ServiceClient.py +++ b/src/service/client/ServiceClient.py @@ -13,6 +13,8 @@ # limitations under the License. import grpc, logging +from common.Constants import ServiceNameEnum +from common.Settings import get_service_host, get_service_port_grpc from common.tools.client.RetryDecorator import retry, delay_exponential from common.tools.grpc.Tools import grpc_message_to_json_string from service.proto.context_pb2 import Empty, Service, ServiceId @@ -24,9 +26,11 @@ DELAY_FUNCTION = delay_exponential(initial=0.01, increment=2.0, maximum=5.0) RETRY_DECORATOR = retry(max_retries=MAX_RETRIES, delay_function=DELAY_FUNCTION, prepare_method_name='connect') class ServiceClient: - def __init__(self, address, port): - self.endpoint = '{:s}:{:s}'.format(str(address), str(port)) - LOGGER.debug('Creating channel to {:s}...'.format(self.endpoint)) + def __init__(self, host=None, port=None): + if not host: host = get_service_host(ServiceNameEnum.SERVICE) + if not port: port = get_service_port_grpc(ServiceNameEnum.SERVICE) + self.endpoint = '{:s}:{:s}'.format(str(host), str(port)) + LOGGER.debug('Creating channel to {:s}...'.format(str(self.endpoint))) self.channel = None self.stub = None self.connect() diff --git a/src/service/service/ServiceService.py b/src/service/service/ServiceService.py index 21945b7d3..356c314c3 100644 --- a/src/service/service/ServiceService.py +++ b/src/service/service/ServiceService.py @@ -12,72 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -import grpc, logging -from concurrent import futures -from grpc_health.v1.health import HealthServicer, OVERALL_HEALTH -from grpc_health.v1.health_pb2 import HealthCheckResponse -from grpc_health.v1.health_pb2_grpc import add_HealthServicer_to_server +from common.Constants import ServiceNameEnum +from common.Settings import get_service_port_grpc from common.orm.backend.BackendEnum import BackendEnum from common.orm.Database import Database from common.orm.Factory import get_database_backend -from context.client.ContextClient import ContextClient -from device.client.DeviceClient import DeviceClient -from service.Config import GRPC_SERVICE_PORT, GRPC_MAX_WORKERS, GRPC_GRACE_PERIOD +from common.tools.service.GenericGrpcService import GenericGrpcService from service.proto.service_pb2_grpc import add_ServiceServiceServicer_to_server from .ServiceServiceServicerImpl import ServiceServiceServicerImpl from .service_handler_api.ServiceHandlerFactory import ServiceHandlerFactory -BIND_ADDRESS = '0.0.0.0' -LOGGER = logging.getLogger(__name__) +class ServiceService(GenericGrpcService): + def __init__(self, service_handler_factory : ServiceHandlerFactory, cls_name: str = __name__) -> None: + port = get_service_port_grpc(ServiceNameEnum.SERVICE) + super().__init__(port, cls_name=cls_name) + database = Database(get_database_backend(backend=BackendEnum.INMEMORY)) + self.service_servicer = ServiceServiceServicerImpl(database, service_handler_factory) -class ServiceService: - def __init__( - self, context_client : ContextClient, device_client : DeviceClient, - service_handler_factory : ServiceHandlerFactory, - address=BIND_ADDRESS, port=GRPC_SERVICE_PORT, max_workers=GRPC_MAX_WORKERS, - grace_period=GRPC_GRACE_PERIOD): - - self.context_client = context_client - self.device_client = device_client - self.service_handler_factory = service_handler_factory - self.address = address - self.port = port - self.endpoint = None - self.max_workers = max_workers - self.grace_period = grace_period - self.service_servicer = None - self.health_servicer = None - self.pool = None - self.server = None - - self.database = Database(get_database_backend(backend=BackendEnum.INMEMORY)) - - def start(self): - self.endpoint = '{:s}:{:s}'.format(str(self.address), str(self.port)) - LOGGER.info('Starting Service (tentative endpoint: {:s}, max_workers: {:s})...'.format( - str(self.endpoint), str(self.max_workers))) - - self.pool = futures.ThreadPoolExecutor(max_workers=self.max_workers) - self.server = grpc.server(self.pool) # , interceptors=(tracer_interceptor,)) - - self.service_servicer = ServiceServiceServicerImpl( - self.context_client, self.device_client, self.database, self.service_handler_factory) + def install_servicers(self): add_ServiceServiceServicer_to_server(self.service_servicer, self.server) - - self.health_servicer = HealthServicer( - experimental_non_blocking=True, experimental_thread_pool=futures.ThreadPoolExecutor(max_workers=1)) - add_HealthServicer_to_server(self.health_servicer, self.server) - - port = self.server.add_insecure_port(self.endpoint) - self.endpoint = '{:s}:{:s}'.format(str(self.address), str(port)) - LOGGER.info('Listening on {:s}...'.format(str(self.endpoint))) - self.server.start() - self.health_servicer.set(OVERALL_HEALTH, HealthCheckResponse.SERVING) # pylint: disable=maybe-no-member - - LOGGER.debug('Service started') - - def stop(self): - LOGGER.debug('Stopping service (grace period {:s} seconds)...'.format(str(self.grace_period))) - self.health_servicer.enter_graceful_shutdown() - self.server.stop(self.grace_period) - LOGGER.debug('Service stopped') diff --git a/src/service/service/ServiceServiceServicerImpl.py b/src/service/service/ServiceServiceServicerImpl.py index 772069932..f34b99d63 100644 --- a/src/service/service/ServiceServiceServicerImpl.py +++ b/src/service/service/ServiceServiceServicerImpl.py @@ -39,13 +39,10 @@ METHOD_NAMES = ['CreateService', 'UpdateService', 'DeleteService'] METRICS = create_metrics(SERVICE_NAME, METHOD_NAMES) class ServiceServiceServicerImpl(ServiceServiceServicer): - def __init__( - self, context_client : ContextClient, device_client : DeviceClient, database : Database, - service_handler_factory : ServiceHandlerFactory): - + def __init__(self, database : Database, service_handler_factory : ServiceHandlerFactory) -> None: LOGGER.debug('Creating Servicer...') - self.context_client = context_client - self.device_client = device_client + self.context_client = ContextClient() + self.device_client = DeviceClient() self.database = database self.service_handler_factory = service_handler_factory LOGGER.debug('Servicer Created') diff --git a/src/service/service/__main__.py b/src/service/service/__main__.py index cc1b00895..1a67a309f 100644 --- a/src/service/service/__main__.py +++ b/src/service/service/__main__.py @@ -14,12 +14,10 @@ import logging, signal, sys, threading from prometheus_client import start_http_server -from common.Settings import get_setting, wait_for_environment_variables -from context.client.ContextClient import ContextClient -from device.client.DeviceClient import DeviceClient -from service.Config import ( - CONTEXT_SERVICE_HOST, CONTEXT_SERVICE_PORT, DEVICE_SERVICE_HOST, DEVICE_SERVICE_PORT, GRPC_SERVICE_PORT, - GRPC_MAX_WORKERS, GRPC_GRACE_PERIOD, LOG_LEVEL, METRICS_PORT) +from common.Constants import ServiceNameEnum +from common.Settings import ( + ENVVAR_SUFIX_SERVICE_HOST, ENVVAR_SUFIX_SERVICE_PORT_GRPC, get_env_var_name, get_log_level, get_metrics_port, + wait_for_environment_variables) from .ServiceService import ServiceService from .service_handler_api.ServiceHandlerFactory import ServiceHandlerFactory from .service_handlers import SERVICE_HANDLERS @@ -34,51 +32,31 @@ def signal_handler(signal, frame): # pylint: disable=redefined-outer-name def main(): global LOGGER # pylint: disable=global-statement - grpc_service_port = get_setting('SERVICESERVICE_SERVICE_PORT_GRPC', default=GRPC_SERVICE_PORT ) - max_workers = get_setting('MAX_WORKERS', default=GRPC_MAX_WORKERS ) - grace_period = get_setting('GRACE_PERIOD', default=GRPC_GRACE_PERIOD ) - log_level = get_setting('LOG_LEVEL', default=LOG_LEVEL ) - metrics_port = get_setting('METRICS_PORT', default=METRICS_PORT ) - + log_level = get_log_level() logging.basicConfig(level=log_level) LOGGER = logging.getLogger(__name__) wait_for_environment_variables([ - 'CONTEXTSERVICE_SERVICE_HOST', 'CONTEXTSERVICE_SERVICE_PORT_GRPC', - 'DEVICESERVICE_SERVICE_HOST', 'DEVICESERVICE_SERVICE_PORT_GRPC' + get_env_var_name(ServiceNameEnum.CONTEXT, ENVVAR_SUFIX_SERVICE_HOST ), + get_env_var_name(ServiceNameEnum.CONTEXT, ENVVAR_SUFIX_SERVICE_PORT_GRPC), + get_env_var_name(ServiceNameEnum.DEVICE, ENVVAR_SUFIX_SERVICE_HOST ), + get_env_var_name(ServiceNameEnum.DEVICE, ENVVAR_SUFIX_SERVICE_PORT_GRPC), ]) - context_service_host = get_setting('CONTEXTSERVICE_SERVICE_HOST', default=CONTEXT_SERVICE_HOST) - context_service_port = get_setting('CONTEXTSERVICE_SERVICE_PORT_GRPC', default=CONTEXT_SERVICE_PORT) - device_service_host = get_setting('DEVICESERVICE_SERVICE_HOST', default=DEVICE_SERVICE_HOST ) - device_service_port = get_setting('DEVICESERVICE_SERVICE_PORT_GRPC', default=DEVICE_SERVICE_PORT ) - signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) LOGGER.info('Starting...') # Start metrics server + metrics_port = get_metrics_port() start_http_server(metrics_port) - # Initialize Context Client - if context_service_host is None or context_service_port is None: - raise Exception('Wrong address({:s}):port({:s}) of Context component'.format( - str(context_service_host), str(context_service_port))) - context_client = ContextClient(context_service_host, context_service_port) - - # Initialize Device Client - if device_service_host is None or device_service_port is None: - raise Exception('Wrong address({:s}):port({:s}) of Device component'.format( - str(device_service_host), str(device_service_port))) - device_client = DeviceClient(device_service_host, device_service_port) - + # Initialize ServiceHandler Factory service_handler_factory = ServiceHandlerFactory(SERVICE_HANDLERS) # Starting service service - grpc_service = ServiceService( - context_client, device_client, service_handler_factory, port=grpc_service_port, max_workers=max_workers, - grace_period=grace_period) + grpc_service = ServiceService(service_handler_factory) grpc_service.start() # Wait for Ctrl+C or termination signal diff --git a/src/service/tests/MockService_Dependencies.py b/src/service/tests/MockService_Dependencies.py new file mode 100644 index 000000000..8194ba943 --- /dev/null +++ b/src/service/tests/MockService_Dependencies.py @@ -0,0 +1,49 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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 os +from typing import Union +from common.Constants import ServiceNameEnum +from common.Settings import ENVVAR_SUFIX_SERVICE_HOST, ENVVAR_SUFIX_SERVICE_PORT_GRPC, get_env_var_name +from common.tests.MockServicerImpl_Context import MockServicerImpl_Context +from common.tests.MockServicerImpl_Device import MockServicerImpl_Device +from common.tools.service.GenericGrpcService import GenericGrpcService +from context.proto.context_pb2_grpc import add_ContextServiceServicer_to_server +from device.proto.device_pb2_grpc import add_DeviceServiceServicer_to_server + +LOCAL_HOST = '127.0.0.1' + +SERVICE_CONTEXT = ServiceNameEnum.CONTEXT +SERVICE_DEVICE = ServiceNameEnum.DEVICE + +class MockService_Dependencies(GenericGrpcService): + # Mock Service implementing Context and Device to simplify unitary tests of Device + + def __init__(self, bind_port: Union[str, int]) -> None: + super().__init__(bind_port, LOCAL_HOST, enable_health_servicer=False, cls_name='MockService') + + # pylint: disable=attribute-defined-outside-init + def install_servicers(self): + self.context_servicer = MockServicerImpl_Context() + add_ContextServiceServicer_to_server(self.context_servicer, self.server) + + self.device_servicer = MockServicerImpl_Device() + add_DeviceServiceServicer_to_server(self.device_servicer, self.server) + + def configure_env_vars(self): + os.environ[get_env_var_name(SERVICE_CONTEXT, ENVVAR_SUFIX_SERVICE_HOST )] = str(self.bind_address) + os.environ[get_env_var_name(SERVICE_CONTEXT, ENVVAR_SUFIX_SERVICE_PORT_GRPC)] = str(self.bind_port) + + os.environ[get_env_var_name(SERVICE_DEVICE, ENVVAR_SUFIX_SERVICE_HOST )] = str(self.bind_address) + os.environ[get_env_var_name(SERVICE_DEVICE, ENVVAR_SUFIX_SERVICE_PORT_GRPC)] = str(self.bind_port) diff --git a/src/service/tests/PrepareTestScenario.py b/src/service/tests/PrepareTestScenario.py new file mode 100644 index 000000000..bcf3cd156 --- /dev/null +++ b/src/service/tests/PrepareTestScenario.py @@ -0,0 +1,68 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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 pytest, os +from common.Constants import ServiceNameEnum +from common.Settings import ( + ENVVAR_SUFIX_SERVICE_HOST, ENVVAR_SUFIX_SERVICE_PORT_GRPC, get_env_var_name, get_service_port_grpc) +from context.client.ContextClient import ContextClient +from device.client.DeviceClient import DeviceClient +from service.client.ServiceClient import ServiceClient +from service.service.ServiceService import ServiceService +from service.service.service_handler_api.ServiceHandlerFactory import ServiceHandlerFactory +from service.service.service_handlers import SERVICE_HANDLERS +from service.tests.MockService_Dependencies import MockService_Dependencies + +LOCAL_HOST = '127.0.0.1' +MOCKSERVICE_PORT = 10000 +SERVICE_SERVICE_PORT = MOCKSERVICE_PORT + get_service_port_grpc(ServiceNameEnum.SERVICE) # avoid privileged ports +os.environ[get_env_var_name(ServiceNameEnum.SERVICE, ENVVAR_SUFIX_SERVICE_HOST )] = str(LOCAL_HOST) +os.environ[get_env_var_name(ServiceNameEnum.SERVICE, ENVVAR_SUFIX_SERVICE_PORT_GRPC)] = str(SERVICE_SERVICE_PORT) + +@pytest.fixture(scope='session') +def mock_service(): + _service = MockService_Dependencies(MOCKSERVICE_PORT) + _service.configure_env_vars() + _service.start() + yield _service + _service.stop() + +@pytest.fixture(scope='session') +def context_client(mock_service : MockService_Dependencies): # pylint: disable=redefined-outer-name + _client = ContextClient() + yield _client + _client.close() + +@pytest.fixture(scope='session') +def device_client(mock_service : MockService_Dependencies): # pylint: disable=redefined-outer-name + _client = DeviceClient() + yield _client + _client.close() + +@pytest.fixture(scope='session') +def service_service( + context_client : ContextClient, # pylint: disable=redefined-outer-name + device_client : DeviceClient): # pylint: disable=redefined-outer-name + + _service_handler_factory = ServiceHandlerFactory(SERVICE_HANDLERS) + _service = ServiceService(_service_handler_factory) + _service.start() + yield _service + _service.stop() + +@pytest.fixture(scope='session') +def service_client(service_service : ServiceService): # pylint: disable=redefined-outer-name + _client = ServiceClient() + yield _client + _client.close() diff --git a/src/service/tests/test_unitary.py b/src/service/tests/test_unitary.py index 812a65c5c..60fd17371 100644 --- a/src/service/tests/test_unitary.py +++ b/src/service/tests/test_unitary.py @@ -12,90 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. -import copy, grpc, logging, os, pytest -from common.tests.MockService import MockService -from common.tests.MockServicerImpl_Context import MockServicerImpl_Context -from common.tests.MockServicerImpl_Device import MockServicerImpl_Device +import copy, grpc, logging, pytest from common.tests.PytestGenerateTests import pytest_generate_tests # (required) pylint: disable=unused-import from common.tools.grpc.Tools import grpc_message_to_json_string from context.client.ContextClient import ContextClient from context.proto.context_pb2 import Context, ContextId, DeviceId, Link, LinkId, Topology, Device, TopologyId -from context.proto.context_pb2_grpc import add_ContextServiceServicer_to_server from device.client.DeviceClient import DeviceClient -from device.proto.device_pb2_grpc import add_DeviceServiceServicer_to_server -from service.Config import ( - GRPC_SERVICE_PORT as SERVICE_GRPC_SERVICE_PORT, GRPC_MAX_WORKERS as SERVICE_GRPC_MAX_WORKERS, - GRPC_GRACE_PERIOD as SERVICE_GRPC_GRACE_PERIOD) from service.client.ServiceClient import ServiceClient from service.proto.context_pb2 import Service, ServiceId -from service.service.ServiceService import ServiceService -from service.service.service_handler_api.ServiceHandlerFactory import ServiceHandlerFactory -from service.service.service_handlers import SERVICE_HANDLERS +from .PrepareTestScenario import ( # pylint: disable=unused-import + # be careful, order of symbols is important here! + mock_service, service_service, context_client, device_client, service_client) LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) -SERVICE_GRPC_SERVICE_PORT = 10000 + SERVICE_GRPC_SERVICE_PORT # avoid privileged ports - -LOCALHOST = '127.0.0.1' -MOCKSERVER_GRPC_PORT = 10000 - -class MockService_Combined(MockService): - # Mock Server implementing Context and Service to simplify unitary tests of Compute - - def __init__(self, cls_name='MockService_Service'): - super().__init__(LOCALHOST, MOCKSERVER_GRPC_PORT, cls_name=cls_name) - - # pylint: disable=attribute-defined-outside-init - def install_servicers(self): - self.context_servicer = MockServicerImpl_Context() - add_ContextServiceServicer_to_server(self.context_servicer, self.server) - self.device_servicer = MockServicerImpl_Device() - add_DeviceServiceServicer_to_server(self.device_servicer, self.server) - -os.environ['CONTEXTSERVICE_SERVICE_HOST'] = LOCALHOST -os.environ['CONTEXTSERVICE_SERVICE_PORT_GRPC'] = str(MOCKSERVER_GRPC_PORT) -os.environ['DEVICESERVICE_SERVICE_HOST'] = LOCALHOST -os.environ['DEVICESERVICE_SERVICE_PORT_GRPC'] = str(MOCKSERVER_GRPC_PORT) - -@pytest.fixture(scope='session') -def mockservice(): - _service = MockService_Combined() - _service.start() - yield _service - _service.stop() - -@pytest.fixture(scope='session') -def context_client(mockservice : MockService_Combined): # pylint: disable=redefined-outer-name - _client = ContextClient(address=LOCALHOST, port=MOCKSERVER_GRPC_PORT) - yield _client - _client.close() - -@pytest.fixture(scope='session') -def device_client(mockservice : MockService_Combined): # pylint: disable=redefined-outer-name - _client = DeviceClient(address=LOCALHOST, port=MOCKSERVER_GRPC_PORT) - yield _client - _client.close() - -@pytest.fixture(scope='session') -def service_service( - context_client : ContextClient, # pylint: disable=redefined-outer-name - device_client : DeviceClient): # pylint: disable=redefined-outer-name - - _service_handler_factory = ServiceHandlerFactory(SERVICE_HANDLERS) - _service = ServiceService( - context_client, device_client, _service_handler_factory, - port=SERVICE_GRPC_SERVICE_PORT, max_workers=SERVICE_GRPC_MAX_WORKERS, grace_period=SERVICE_GRPC_GRACE_PERIOD) - _service.start() - yield _service - _service.stop() - -@pytest.fixture(scope='session') -def service_client(service_service : ServiceService): # pylint: disable=redefined-outer-name - _client = ServiceClient(address=LOCALHOST, port=SERVICE_GRPC_SERVICE_PORT) - yield _client - _client.close() - try: from .ServiceHandlersToTest import SERVICE_HANDLERS_TO_TEST except ImportError: -- GitLab