From 11e372c4898a0d82b3304f6b68f3fb57ff9a5abe Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Tue, 31 Oct 2023 21:37:34 +0000 Subject: [PATCH 01/29] Fixes http endpoint being overwritten by gRPC address argument in constructor (#621) * Fixes bug of constructor argument being used as http endpoint Signed-off-by: Elena Kolevska * Updates tests Signed-off-by: Elena Kolevska * Updates the docs explaining how http service invocation should be configured Signed-off-by: Elena Kolevska * Update daprdocs/content/en/python-sdk-docs/python-client.md Signed-off-by: Bernd Verst --------- Signed-off-by: Elena Kolevska Signed-off-by: Bernd Verst Co-authored-by: Bernd Verst Signed-off-by: Elena Kolevska # Conflicts: # dapr/clients/__init__.py # dapr/clients/http/client.py # dapr/clients/http/dapr_invocation_http_client.py # tests/clients/test_http_service_invocation_client.py # tests/clients/test_secure_http_service_invocation_client.py --- dapr/clients/__init__.py | 5 ++--- dapr/clients/http/client.py | 22 ++++++------------- .../http/dapr_invocation_http_client.py | 10 ++++----- .../en/python-sdk-docs/python-client.md | 8 +++++++ .../test_http_service_invocation_client.py | 11 +++++----- ...t_secure_http_service_invocation_client.py | 13 ++++++++++- 6 files changed, 38 insertions(+), 31 deletions(-) diff --git a/dapr/clients/__init__.py b/dapr/clients/__init__.py index 1cdfdebb..d1ea3ca8 100644 --- a/dapr/clients/__init__.py +++ b/dapr/clients/__init__.py @@ -84,9 +84,8 @@ def __init__( if invocation_protocol == 'HTTP': if http_timeout_seconds is None: http_timeout_seconds = settings.DAPR_HTTP_TIMEOUT_SECONDS - self.invocation_client = DaprInvocationHttpClient( - headers_callback=headers_callback, timeout=http_timeout_seconds, address=address - ) + self.invocation_client = DaprInvocationHttpClient(headers_callback=headers_callback, + timeout=http_timeout_seconds) elif invocation_protocol == 'GRPC': pass else: diff --git a/dapr/clients/http/client.py b/dapr/clients/http/client.py index 4ac5271e..863e69c1 100644 --- a/dapr/clients/http/client.py +++ b/dapr/clients/http/client.py @@ -33,14 +33,10 @@ class DaprHttpClient: """A Dapr Http API client""" - - def __init__( - self, - message_serializer: 'Serializer', - timeout: Optional[int] = 60, - headers_callback: Optional[Callable[[], Dict[str, str]]] = None, - address: Optional[str] = None, - ): + def __init__(self, + message_serializer: 'Serializer', + timeout: Optional[int] = 60, + headers_callback: Optional[Callable[[], Dict[str, str]]] = None): """Invokes Dapr over HTTP. Args: @@ -51,17 +47,13 @@ def __init__( self._timeout = aiohttp.ClientTimeout(total=timeout) self._serializer = message_serializer self._headers_callback = headers_callback - self._address = address def get_api_url(self) -> str: - if self._address: - return '{}/{}'.format(self._address, settings.DAPR_API_VERSION) if settings.DAPR_HTTP_ENDPOINT: return '{}/{}'.format(settings.DAPR_HTTP_ENDPOINT, settings.DAPR_API_VERSION) - else: - return 'http://{}:{}/{}'.format( - settings.DAPR_RUNTIME_HOST, settings.DAPR_HTTP_PORT, settings.DAPR_API_VERSION - ) + + return 'http://{}:{}/{}'.format(settings.DAPR_RUNTIME_HOST, + settings.DAPR_HTTP_PORT, settings.DAPR_API_VERSION) async def send_bytes( self, diff --git a/dapr/clients/http/dapr_invocation_http_client.py b/dapr/clients/http/dapr_invocation_http_client.py index 462f0d1a..b1923d46 100644 --- a/dapr/clients/http/dapr_invocation_http_client.py +++ b/dapr/clients/http/dapr_invocation_http_client.py @@ -32,18 +32,16 @@ class DaprInvocationHttpClient: """Service Invocation HTTP Client""" def __init__( - self, - timeout: int = 60, - headers_callback: Optional[Callable[[], Dict[str, str]]] = None, - address: Optional[str] = None, - ): + self, + timeout: int = 60, + headers_callback: Optional[Callable[[], Dict[str, str]]] = None): """Invokes Dapr's API for method invocation over HTTP. Args: timeout (int, optional): Timeout in seconds, defaults to 60. headers_callback (lambda: Dict[str, str]], optional): Generates header for each request. """ - self._client = DaprHttpClient(DefaultJSONSerializer(), timeout, headers_callback, address) + self._client = DaprHttpClient(DefaultJSONSerializer(), timeout, headers_callback) async def invoke_method_async( self, diff --git a/daprdocs/content/en/python-sdk-docs/python-client.md b/daprdocs/content/en/python-sdk-docs/python-client.md index 94370003..47f76c83 100644 --- a/daprdocs/content/en/python-sdk-docs/python-client.md +++ b/daprdocs/content/en/python-sdk-docs/python-client.md @@ -101,6 +101,8 @@ The Python SDK allows you to interface with all of the [Dapr building blocks]({{ ### Invoke a service +The Dapr Python SDK provides a simple API for invoking services via either HTTP or gRPC (deprecated). The protocol can be selected by setting the `DAPR_API_METHOD_INVOCATION_PROTOCOL` environment variable, defaulting to HTTP when unset. GRPC service invocation in Dapr is deprecated and GRPC proxying is recommended as an alternative. + ```python from dapr.clients import DaprClient @@ -113,6 +115,12 @@ with DaprClient() as d: resp = d.invoke_method('service-to-invoke', 'method-to-invoke', data='{"id":"100", "FirstName":"Value", "LastName":"Value"}', http_verb='post') ``` +The base endpoint for HTTP api calls is specified in the `DAPR_HTTP_ENDPOINT` environment variable. +If this variable is not set, the endpoint value is derived from the `DAPR_RUNTIME_HOST` and `DAPR_HTTP_PORT` variables, whose default values are `127.0.0.1` and `3500` accordingly. + +The base endpoint for gRPC calls is the one used for the client initialisation ([explained above](#initialising-the-client)). + + - For a full guide on service invocation visit [How-To: Invoke a service]({{< ref howto-invoke-discover-services.md >}}). - Visit [Python SDK examples](https://github.com/dapr/python-sdk/tree/master/examples/invoke-simple) for code samples and instructions to try out service invocation. diff --git a/tests/clients/test_http_service_invocation_client.py b/tests/clients/test_http_service_invocation_client.py index 83d8b433..8ab8659e 100644 --- a/tests/clients/test_http_service_invocation_client.py +++ b/tests/clients/test_http_service_invocation_client.py @@ -59,12 +59,11 @@ def test_get_api_url_default(self): client.invocation_client._client.get_api_url(), ) - def test_get_api_url_endpoint_as_argument(self): - client = DaprClient('http://localhost:5000') - self.assertEqual( - 'http://localhost:5000/{}'.format(settings.DAPR_API_VERSION), - client.invocation_client._client.get_api_url(), - ) + @patch.object(settings, "DAPR_HTTP_ENDPOINT", "https://domain1.com:5000") + def test_dont_get_api_url_endpoint_as_argument(self): + client = DaprClient("http://localhost:5000") + self.assertEqual('https://domain1.com:5000/{}'.format(settings.DAPR_API_VERSION), + client.invocation_client._client.get_api_url()) @patch.object(settings, 'DAPR_HTTP_ENDPOINT', 'https://domain1.com:5000') def test_get_api_url_endpoint_as_env_variable(self): diff --git a/tests/clients/test_secure_http_service_invocation_client.py b/tests/clients/test_secure_http_service_invocation_client.py index 4a85367b..a499c60a 100644 --- a/tests/clients/test_secure_http_service_invocation_client.py +++ b/tests/clients/test_secure_http_service_invocation_client.py @@ -15,6 +15,7 @@ import ssl import typing from asyncio import TimeoutError +from unittest.mock import patch from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider @@ -27,6 +28,7 @@ from dapr.conf import settings from dapr.proto import common_v1 + from .certs import CERTIFICATE_CHAIN_PATH from .fake_http_server import FakeHttpServer from .test_http_service_invocation_client import DaprInvocationHttpClientTests @@ -48,7 +50,8 @@ def setUp(self): self.server.start() settings.DAPR_HTTP_PORT = self.server_port settings.DAPR_API_METHOD_INVOCATION_PROTOCOL = 'http' - self.client = DaprClient(f'https://localhost:{self.server_port}') + settings.DAPR_HTTP_ENDPOINT = "https://localhost:{}".format(self.server_port) + self.client = DaprClient() self.app_id = 'fakeapp' self.method_name = 'fakemethod' self.invoke_url = f'/v1.0/invoke/{self.app_id}/method/{self.method_name}' @@ -114,3 +117,11 @@ def test_timeout_exception_thrown_when_timeout_reached(self): self.server.set_server_delay(1.5) with self.assertRaises(TimeoutError): new_client.invoke_method(self.app_id, self.method_name, '') + + @patch.object(settings, "DAPR_HTTP_ENDPOINT", None) + def test_get_api_url_default(self): + client = DaprClient() + self.assertEqual( + 'http://{}:{}/{}'.format(settings.DAPR_RUNTIME_HOST, settings.DAPR_HTTP_PORT, + settings.DAPR_API_VERSION), + client.invocation_client._client.get_api_url()) From a1275e2c7af57a52e11cf4f5498c6bd2ebae4b6c Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Wed, 7 Feb 2024 21:55:00 +0000 Subject: [PATCH 02/29] health decorator - first commit Signed-off-by: Elena Kolevska --- dapr/clients/health.py | 80 +++++++++ dapr/clients/http/client.py | 7 - dapr/clients/http/dapr_actor_http_client.py | 4 +- .../http/dapr_invocation_http_client.py | 3 +- dapr/clients/http/helpers.py | 24 +++ dapr/conf/helpers.py | 22 +-- examples/state_store/state_store.py | 159 ++++++++---------- .../test_http_service_invocation_client.py | 13 +- ...t_secure_http_service_invocation_client.py | 9 +- 9 files changed, 203 insertions(+), 118 deletions(-) create mode 100644 dapr/clients/health.py create mode 100644 dapr/clients/http/helpers.py diff --git a/dapr/clients/health.py b/dapr/clients/health.py new file mode 100644 index 00000000..18e703c9 --- /dev/null +++ b/dapr/clients/health.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2024 The Dapr Authors +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 asyncio +import inspect +import urllib.request +import urllib.error +import time +from functools import wraps + +import aiohttp + +from dapr.clients.http.client import DAPR_API_TOKEN_HEADER, USER_AGENT_HEADER, DAPR_USER_AGENT +from dapr.clients.http.helpers import get_api_url +from dapr.conf import settings + + +def healthcheck(timeout_s: int = 5): + def decorator(func): + healthz_url = f'{get_api_url()}/healthz/outbound' + headers = {USER_AGENT_HEADER: DAPR_USER_AGENT} + if settings.DAPR_API_TOKEN is not None: + headers[DAPR_API_TOKEN_HEADER] = settings.DAPR_API_TOKEN + start = time.time() + + @wraps(func) + async def async_wrapper(*args, **kwargs): + # Async health check logic + async with aiohttp.ClientSession() as session: + while True: + try: + async with session.get(healthz_url, headers=headers) as response: + if 200 <= response.status < 300: + print("Dapr service is healthy.") + break + except aiohttp.ClientError as e: + print(f"Health check failed: {e}") + + remaining = (start + timeout_s) - time.time() + if remaining <= 0: + print("Health check timed out.") + return False + await asyncio.sleep(min(1, remaining)) + return await func(*args, **kwargs) + + def sync_wrapper(*args, **kwargs): + while True: + try: + req = urllib.request.Request(healthz_url, headers=headers) + with urllib.request.urlopen(req) as response: + if 200 <= response.status < 300: + print("Dapr service is healthy. SYNC") + break + except urllib.error.URLError as e: + print(f"Health check failed: {e.reason}") + + remaining = (start + timeout_s) - time.time() + if remaining <= 0: + print("Health check timed out.") + return False + time.sleep(min(1, remaining)) + return func(*args, **kwargs) + + if inspect.iscoroutinefunction(func): + return async_wrapper + else: + return sync_wrapper + + return decorator diff --git a/dapr/clients/http/client.py b/dapr/clients/http/client.py index 863e69c1..746a2a3c 100644 --- a/dapr/clients/http/client.py +++ b/dapr/clients/http/client.py @@ -48,13 +48,6 @@ def __init__(self, self._serializer = message_serializer self._headers_callback = headers_callback - def get_api_url(self) -> str: - if settings.DAPR_HTTP_ENDPOINT: - return '{}/{}'.format(settings.DAPR_HTTP_ENDPOINT, settings.DAPR_API_VERSION) - - return 'http://{}:{}/{}'.format(settings.DAPR_RUNTIME_HOST, - settings.DAPR_HTTP_PORT, settings.DAPR_API_VERSION) - async def send_bytes( self, method: str, diff --git a/dapr/clients/http/dapr_actor_http_client.py b/dapr/clients/http/dapr_actor_http_client.py index 0a462587..a4fccfc1 100644 --- a/dapr/clients/http/dapr_actor_http_client.py +++ b/dapr/clients/http/dapr_actor_http_client.py @@ -15,6 +15,8 @@ from typing import Callable, Dict, Optional, Union, TYPE_CHECKING +from dapr.clients.http.helpers import get_api_url + if TYPE_CHECKING: from dapr.serializers import Serializer @@ -145,4 +147,4 @@ async def unregister_timer(self, actor_type: str, actor_id: str, name: str) -> N await self._client.send_bytes(method='DELETE', url=url, data=None) def _get_base_url(self, actor_type: str, actor_id: str) -> str: - return '{}/actors/{}/{}'.format(self._client.get_api_url(), actor_type, actor_id) + return '{}/actors/{}/{}'.format(get_api_url(), actor_type, actor_id) diff --git a/dapr/clients/http/dapr_invocation_http_client.py b/dapr/clients/http/dapr_invocation_http_client.py index b1923d46..7047d00c 100644 --- a/dapr/clients/http/dapr_invocation_http_client.py +++ b/dapr/clients/http/dapr_invocation_http_client.py @@ -21,6 +21,7 @@ from dapr.clients.http.client import DaprHttpClient, CONTENT_TYPE_HEADER from dapr.clients.grpc._helpers import MetadataTuple, GrpcMessage from dapr.clients.grpc._response import InvokeMethodResponse +from dapr.clients.http.helpers import get_api_url from dapr.serializers import DefaultJSONSerializer from dapr.version import __version__ @@ -89,7 +90,7 @@ async def invoke_method_async( headers[USER_AGENT_HEADER] = DAPR_USER_AGENT - url = f'{self._client.get_api_url()}/invoke/{app_id}/method/{method_name}' + url = f'{get_api_url()}/invoke/{app_id}/method/{method_name}' if isinstance(data, GrpcMessage): body = data.SerializeToString() diff --git a/dapr/clients/http/helpers.py b/dapr/clients/http/helpers.py new file mode 100644 index 00000000..bf1e40c1 --- /dev/null +++ b/dapr/clients/http/helpers.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2023 The Dapr Authors +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. +""" + +from dapr.conf import settings + + +def get_api_url() -> str: + if settings.DAPR_HTTP_ENDPOINT: + return '{}/{}'.format(settings.DAPR_HTTP_ENDPOINT, settings.DAPR_API_VERSION) + + return 'http://{}:{}/{}'.format(settings.DAPR_RUNTIME_HOST, settings.DAPR_HTTP_PORT, + settings.DAPR_API_VERSION) diff --git a/dapr/conf/helpers.py b/dapr/conf/helpers.py index 8740bde8..aa004a1a 100644 --- a/dapr/conf/helpers.py +++ b/dapr/conf/helpers.py @@ -123,11 +123,8 @@ def _preprocess_uri(self, url: str) -> str: if len(url_list) == 3 and '://' not in url: # A URI like dns:mydomain:5000 or vsock:mycid:5000 was used url = url.replace(':', '://', 1) - elif ( - len(url_list) >= 2 - and '://' not in url - and url_list[0] in URIParseConfig.ACCEPTED_SCHEMES - ): + elif (len(url_list) >= 2 and '://' not in url and url_list[ + 0] in URIParseConfig.ACCEPTED_SCHEMES): # A URI like dns:mydomain or dns:[2001:db8:1f70::999:de8:7648:6e8]:mydomain was used # Possibly a URI like dns:[2001:db8:1f70::999:de8:7648:6e8]:mydomain was used url = url.replace(':', '://', 1) @@ -172,18 +169,13 @@ def tls(self) -> bool: def _validate_path_and_query(self) -> None: if self._parsed_url.path: raise ValueError( - f'paths are not supported for gRPC endpoints:' f" '{self._parsed_url.path}'" - ) + f'paths are not supported for gRPC endpoints:' f" '{self._parsed_url.path}'") if self._parsed_url.query: query_dict = parse_qs(self._parsed_url.query) if 'tls' in query_dict and self._parsed_url.scheme in ['http', 'https']: - raise ValueError( - f'the tls query parameter is not supported for http(s) endpoints: ' - f"'{self._parsed_url.query}'" - ) + raise ValueError(f'the tls query parameter is not supported for http(s) endpoints: ' + f"'{self._parsed_url.query}'") query_dict.pop('tls', None) if query_dict: - raise ValueError( - f'query parameters are not supported for gRPC endpoints:' - f" '{self._parsed_url.query}'" - ) + raise ValueError(f'query parameters are not supported for gRPC endpoints:' + f" '{self._parsed_url.query}'") diff --git a/examples/state_store/state_store.py b/examples/state_store/state_store.py index 411e809a..fa222b24 100644 --- a/examples/state_store/state_store.py +++ b/examples/state_store/state_store.py @@ -8,89 +8,76 @@ from dapr.clients.grpc._request import TransactionalStateOperation, TransactionOperationType from dapr.clients.grpc._state import StateItem - -with DaprClient() as d: - storeName = 'statestore' - - key = 'key_1' - value = 'value_1' - updated_value = 'value_1_updated' - - another_key = 'key_2' - another_value = 'value_2' - - yet_another_key = 'key_3' - yet_another_value = 'value_3' - - # Wait for sidecar to be up within 5 seconds. - d.wait(5) - - # Save single state. - d.save_state(store_name=storeName, key=key, value=value) - print(f'State store has successfully saved {value} with {key} as key') - - # Save with an etag that is different from the one stored in the database. - try: - d.save_state(store_name=storeName, key=key, value=another_value, etag='9999') - except grpc.RpcError as err: - # StatusCode should be StatusCode.ABORTED. - print(f'Cannot save due to bad etag. ErrorCode={err.code()}') - - # For detailed error messages from the dapr runtime: - # print(f"Details={err.details()}) - - # Save multiple states. - d.save_bulk_state( - store_name=storeName, - states=[ - StateItem(key=another_key, value=another_value), - StateItem(key=yet_another_key, value=yet_another_value), - ], - ) - print(f'State store has successfully saved {another_value} with {another_key} as key') - print(f'State store has successfully saved {yet_another_value} with {yet_another_key} as key') - - # Save bulk with etag that is different from the one stored in the database. - try: - d.save_bulk_state( - store_name=storeName, - states=[ - StateItem(key=another_key, value=another_value, etag='999'), - StateItem(key=yet_another_key, value=yet_another_value, etag='999'), - ], - ) - except grpc.RpcError as err: - # StatusCode should be StatusCode.ABORTED. - print(f'Cannot save bulk due to bad etags. ErrorCode={err.code()}') - - # For detailed error messages from the dapr runtime: - # print(f"Details={err.details()}) - - # Get one state by key. - state = d.get_state(store_name=storeName, key=key, state_metadata={'metakey': 'metavalue'}) - print(f'Got value={state.data} eTag={state.etag}') - - # Transaction upsert - d.execute_state_transaction( - store_name=storeName, - operations=[ - TransactionalStateOperation( - operation_type=TransactionOperationType.upsert, - key=key, - data=updated_value, - etag=state.etag, - ), - TransactionalStateOperation(key=another_key, data=another_value), - ], - ) - - # Batch get - items = d.get_bulk_state( - store_name=storeName, keys=[key, another_key], states_metadata={'metakey': 'metavalue'} - ).items - print(f'Got items with etags: {[(i.data, i.etag) for i in items]}') - - # Delete one state by key. - d.delete_state(store_name=storeName, key=key, state_metadata={'metakey': 'metavalue'}) - data = d.get_state(store_name=storeName, key=key).data - print(f'Got value after delete: {data}') +from dapr.clients.health import healthcheck + + +@healthcheck(5) +def save_state(): + with DaprClient() as d: + storeName = 'statestore' + + key = 'key_1' + value = 'value_1' + updated_value = 'value_1_updated' + + another_key = 'key_2' + another_value = 'value_2' + + yet_another_key = 'key_3' + yet_another_value = 'value_3' + + # Save single state. + d.save_state(store_name=storeName, key=key, value=value) + print(f'State store has successfully saved {value} with {key} as key') + + # Save with an etag that is different from the one stored in the database. + try: + d.save_state(store_name=storeName, key=key, value=another_value, etag='9999') + except grpc.RpcError as err: + # StatusCode should be StatusCode.ABORTED. + print(f'Cannot save due to bad etag. ErrorCode={err.code()}') + + # For detailed error messages from the dapr runtime: # print(f"Details={err.details()}) + + # Save multiple states. + d.save_bulk_state(store_name=storeName, + states=[StateItem(key=another_key, value=another_value), + StateItem(key=yet_another_key, value=yet_another_value), ], ) + print(f'State store has successfully saved {another_value} with {another_key} as key') + print( + f'State store has successfully saved {yet_another_value} with {yet_another_key} as key') + + # Save bulk with etag that is different from the one stored in the database. + try: + d.save_bulk_state(store_name=storeName, + states=[StateItem(key=another_key, value=another_value, etag='999'), + StateItem(key=yet_another_key, value=yet_another_value, + etag='999'), ], ) + except grpc.RpcError as err: + # StatusCode should be StatusCode.ABORTED. + print(f'Cannot save bulk due to bad etags. ErrorCode={err.code()}') + + # For detailed error messages from the dapr runtime: # print(f"Details={err.details()}) + + # Get one state by key. + state = d.get_state(store_name=storeName, key=key, state_metadata={'metakey': 'metavalue'}) + print(f'Got value={state.data} eTag={state.etag}') + + # Transaction upsert + d.execute_state_transaction(store_name=storeName, operations=[ + TransactionalStateOperation(operation_type=TransactionOperationType.upsert, key=key, + data=updated_value, etag=state.etag, ), + TransactionalStateOperation(key=another_key, data=another_value), ], ) + + # Batch get + items = d.get_bulk_state(store_name=storeName, keys=[key, another_key], + states_metadata={'metakey': 'metavalue'}).items + print(f'Got items with etags: {[(i.data, i.etag) for i in items]}') + + # Delete one state by key. + d.delete_state(store_name=storeName, key=key, state_metadata={'metakey': 'metavalue'}) + data = d.get_state(store_name=storeName, key=key).data + print(f'Got value after delete: {data}') + + +save_state() diff --git a/tests/clients/test_http_service_invocation_client.py b/tests/clients/test_http_service_invocation_client.py index 8ab8659e..6443cbc6 100644 --- a/tests/clients/test_http_service_invocation_client.py +++ b/tests/clients/test_http_service_invocation_client.py @@ -25,6 +25,7 @@ from opentelemetry.sdk.trace.sampling import ALWAYS_ON from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator +import dapr.clients.http.helpers from dapr.clients import DaprClient from dapr.clients.exceptions import DaprInternalError from dapr.conf import settings @@ -56,21 +57,23 @@ def test_get_api_url_default(self): 'http://{}:{}/{}'.format( settings.DAPR_RUNTIME_HOST, settings.DAPR_HTTP_PORT, settings.DAPR_API_VERSION ), - client.invocation_client._client.get_api_url(), + dapr.clients.http.helpers.get_api_url(), ) @patch.object(settings, "DAPR_HTTP_ENDPOINT", "https://domain1.com:5000") def test_dont_get_api_url_endpoint_as_argument(self): - client = DaprClient("http://localhost:5000") - self.assertEqual('https://domain1.com:5000/{}'.format(settings.DAPR_API_VERSION), - client.invocation_client._client.get_api_url()) + client = DaprClient('http://localhost:5000') + self.assertEqual( + 'https://domain1.com:5000/{}'.format(settings.DAPR_API_VERSION), + dapr.clients.http.helpers.get_api_url(), + ) @patch.object(settings, 'DAPR_HTTP_ENDPOINT', 'https://domain1.com:5000') def test_get_api_url_endpoint_as_env_variable(self): client = DaprClient() self.assertEqual( 'https://domain1.com:5000/{}'.format(settings.DAPR_API_VERSION), - client.invocation_client._client.get_api_url(), + dapr.clients.http.helpers.get_api_url(), ) def test_basic_invoke(self): diff --git a/tests/clients/test_secure_http_service_invocation_client.py b/tests/clients/test_secure_http_service_invocation_client.py index a499c60a..820bd30e 100644 --- a/tests/clients/test_secure_http_service_invocation_client.py +++ b/tests/clients/test_secure_http_service_invocation_client.py @@ -23,6 +23,7 @@ from opentelemetry.sdk.trace.sampling import ALWAYS_ON from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator +import dapr.clients.http.helpers from dapr.clients import DaprClient from dapr.clients.http.client import DaprHttpClient from dapr.conf import settings @@ -122,6 +123,8 @@ def test_timeout_exception_thrown_when_timeout_reached(self): def test_get_api_url_default(self): client = DaprClient() self.assertEqual( - 'http://{}:{}/{}'.format(settings.DAPR_RUNTIME_HOST, settings.DAPR_HTTP_PORT, - settings.DAPR_API_VERSION), - client.invocation_client._client.get_api_url()) + 'http://{}:{}/{}'.format( + settings.DAPR_RUNTIME_HOST, settings.DAPR_HTTP_PORT, settings.DAPR_API_VERSION + ), + dapr.clients.http.helpers.get_api_url(), + ) From 7ff39219b8a671a371a8c490f1c8cd645644ee5e Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Wed, 7 Feb 2024 23:46:40 +0000 Subject: [PATCH 03/29] Fixes tests Signed-off-by: Elena Kolevska --- tests/clients/test_http_helpers.py | 22 +++++++++++++++ .../test_http_service_invocation_client.py | 27 ------------------- ...t_secure_http_service_invocation_client.py | 12 +-------- 3 files changed, 23 insertions(+), 38 deletions(-) create mode 100644 tests/clients/test_http_helpers.py diff --git a/tests/clients/test_http_helpers.py b/tests/clients/test_http_helpers.py new file mode 100644 index 00000000..ab173cd7 --- /dev/null +++ b/tests/clients/test_http_helpers.py @@ -0,0 +1,22 @@ +import unittest +from unittest.mock import patch + +from dapr.conf import settings +from dapr.clients.http.helpers import get_api_url + + +class DaprHttpClientHelpersTests(unittest.TestCase): + def test_get_api_url_default(self, dapr=None): + self.assertEqual( + 'http://{}:{}/{}'.format( + settings.DAPR_RUNTIME_HOST, settings.DAPR_HTTP_PORT, settings.DAPR_API_VERSION + ), + get_api_url(), + ) + + @patch.object(settings, 'DAPR_HTTP_ENDPOINT', 'https://domain1.com:5000') + def test_get_api_url_endpoint_as_env_variable(self): + self.assertEqual( + 'https://domain1.com:5000/{}'.format(settings.DAPR_API_VERSION), + get_api_url(), + ) diff --git a/tests/clients/test_http_service_invocation_client.py b/tests/clients/test_http_service_invocation_client.py index 6443cbc6..f40c5d0d 100644 --- a/tests/clients/test_http_service_invocation_client.py +++ b/tests/clients/test_http_service_invocation_client.py @@ -17,7 +17,6 @@ import typing import unittest from asyncio import TimeoutError -from unittest.mock import patch from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider @@ -25,7 +24,6 @@ from opentelemetry.sdk.trace.sampling import ALWAYS_ON from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator -import dapr.clients.http.helpers from dapr.clients import DaprClient from dapr.clients.exceptions import DaprInternalError from dapr.conf import settings @@ -51,31 +49,6 @@ def tearDown(self): settings.DAPR_API_TOKEN = None settings.DAPR_API_METHOD_INVOCATION_PROTOCOL = 'http' - def test_get_api_url_default(self): - client = DaprClient() - self.assertEqual( - 'http://{}:{}/{}'.format( - settings.DAPR_RUNTIME_HOST, settings.DAPR_HTTP_PORT, settings.DAPR_API_VERSION - ), - dapr.clients.http.helpers.get_api_url(), - ) - - @patch.object(settings, "DAPR_HTTP_ENDPOINT", "https://domain1.com:5000") - def test_dont_get_api_url_endpoint_as_argument(self): - client = DaprClient('http://localhost:5000') - self.assertEqual( - 'https://domain1.com:5000/{}'.format(settings.DAPR_API_VERSION), - dapr.clients.http.helpers.get_api_url(), - ) - - @patch.object(settings, 'DAPR_HTTP_ENDPOINT', 'https://domain1.com:5000') - def test_get_api_url_endpoint_as_env_variable(self): - client = DaprClient() - self.assertEqual( - 'https://domain1.com:5000/{}'.format(settings.DAPR_API_VERSION), - dapr.clients.http.helpers.get_api_url(), - ) - def test_basic_invoke(self): self.server.set_response(b'STRING_BODY') diff --git a/tests/clients/test_secure_http_service_invocation_client.py b/tests/clients/test_secure_http_service_invocation_client.py index 820bd30e..cf16dbf2 100644 --- a/tests/clients/test_secure_http_service_invocation_client.py +++ b/tests/clients/test_secure_http_service_invocation_client.py @@ -51,7 +51,7 @@ def setUp(self): self.server.start() settings.DAPR_HTTP_PORT = self.server_port settings.DAPR_API_METHOD_INVOCATION_PROTOCOL = 'http' - settings.DAPR_HTTP_ENDPOINT = "https://localhost:{}".format(self.server_port) + settings.DAPR_HTTP_ENDPOINT = 'https://localhost:{}'.format(self.server_port) self.client = DaprClient() self.app_id = 'fakeapp' self.method_name = 'fakemethod' @@ -118,13 +118,3 @@ def test_timeout_exception_thrown_when_timeout_reached(self): self.server.set_server_delay(1.5) with self.assertRaises(TimeoutError): new_client.invoke_method(self.app_id, self.method_name, '') - - @patch.object(settings, "DAPR_HTTP_ENDPOINT", None) - def test_get_api_url_default(self): - client = DaprClient() - self.assertEqual( - 'http://{}:{}/{}'.format( - settings.DAPR_RUNTIME_HOST, settings.DAPR_HTTP_PORT, settings.DAPR_API_VERSION - ), - dapr.clients.http.helpers.get_api_url(), - ) From 15a0b1ac67f1bd3d42ffecb562d8a53fd27b6999 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Wed, 7 Feb 2024 23:48:17 +0000 Subject: [PATCH 04/29] Removes unused imports Signed-off-by: Elena Kolevska --- tests/clients/test_secure_http_service_invocation_client.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/clients/test_secure_http_service_invocation_client.py b/tests/clients/test_secure_http_service_invocation_client.py index cf16dbf2..842faae8 100644 --- a/tests/clients/test_secure_http_service_invocation_client.py +++ b/tests/clients/test_secure_http_service_invocation_client.py @@ -15,7 +15,6 @@ import ssl import typing from asyncio import TimeoutError -from unittest.mock import patch from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider @@ -23,7 +22,6 @@ from opentelemetry.sdk.trace.sampling import ALWAYS_ON from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator -import dapr.clients.http.helpers from dapr.clients import DaprClient from dapr.clients.http.client import DaprHttpClient from dapr.conf import settings From d51f97b4416c232a600f72c303a1255d4ad266df Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Wed, 7 Feb 2024 23:50:11 +0000 Subject: [PATCH 05/29] Ruff format Signed-off-by: Elena Kolevska --- dapr/clients/__init__.py | 5 ++- dapr/clients/health.py | 12 ++--- dapr/clients/http/client.py | 11 +++-- .../http/dapr_invocation_http_client.py | 5 +-- dapr/clients/http/helpers.py | 5 ++- dapr/conf/helpers.py | 22 ++++++--- examples/state_store/state_store.py | 45 +++++++++++++------ 7 files changed, 67 insertions(+), 38 deletions(-) diff --git a/dapr/clients/__init__.py b/dapr/clients/__init__.py index d1ea3ca8..042f0dd4 100644 --- a/dapr/clients/__init__.py +++ b/dapr/clients/__init__.py @@ -84,8 +84,9 @@ def __init__( if invocation_protocol == 'HTTP': if http_timeout_seconds is None: http_timeout_seconds = settings.DAPR_HTTP_TIMEOUT_SECONDS - self.invocation_client = DaprInvocationHttpClient(headers_callback=headers_callback, - timeout=http_timeout_seconds) + self.invocation_client = DaprInvocationHttpClient( + headers_callback=headers_callback, timeout=http_timeout_seconds + ) elif invocation_protocol == 'GRPC': pass else: diff --git a/dapr/clients/health.py b/dapr/clients/health.py index 18e703c9..b4f8743f 100644 --- a/dapr/clients/health.py +++ b/dapr/clients/health.py @@ -42,14 +42,14 @@ async def async_wrapper(*args, **kwargs): try: async with session.get(healthz_url, headers=headers) as response: if 200 <= response.status < 300: - print("Dapr service is healthy.") + print('Dapr service is healthy.') break except aiohttp.ClientError as e: - print(f"Health check failed: {e}") + print(f'Health check failed: {e}') remaining = (start + timeout_s) - time.time() if remaining <= 0: - print("Health check timed out.") + print('Health check timed out.') return False await asyncio.sleep(min(1, remaining)) return await func(*args, **kwargs) @@ -60,14 +60,14 @@ def sync_wrapper(*args, **kwargs): req = urllib.request.Request(healthz_url, headers=headers) with urllib.request.urlopen(req) as response: if 200 <= response.status < 300: - print("Dapr service is healthy. SYNC") + print('Dapr service is healthy. SYNC') break except urllib.error.URLError as e: - print(f"Health check failed: {e.reason}") + print(f'Health check failed: {e.reason}') remaining = (start + timeout_s) - time.time() if remaining <= 0: - print("Health check timed out.") + print('Health check timed out.') return False time.sleep(min(1, remaining)) return func(*args, **kwargs) diff --git a/dapr/clients/http/client.py b/dapr/clients/http/client.py index 746a2a3c..e5a12e11 100644 --- a/dapr/clients/http/client.py +++ b/dapr/clients/http/client.py @@ -33,10 +33,13 @@ class DaprHttpClient: """A Dapr Http API client""" - def __init__(self, - message_serializer: 'Serializer', - timeout: Optional[int] = 60, - headers_callback: Optional[Callable[[], Dict[str, str]]] = None): + + def __init__( + self, + message_serializer: 'Serializer', + timeout: Optional[int] = 60, + headers_callback: Optional[Callable[[], Dict[str, str]]] = None, + ): """Invokes Dapr over HTTP. Args: diff --git a/dapr/clients/http/dapr_invocation_http_client.py b/dapr/clients/http/dapr_invocation_http_client.py index 7047d00c..cbffb34e 100644 --- a/dapr/clients/http/dapr_invocation_http_client.py +++ b/dapr/clients/http/dapr_invocation_http_client.py @@ -33,9 +33,8 @@ class DaprInvocationHttpClient: """Service Invocation HTTP Client""" def __init__( - self, - timeout: int = 60, - headers_callback: Optional[Callable[[], Dict[str, str]]] = None): + self, timeout: int = 60, headers_callback: Optional[Callable[[], Dict[str, str]]] = None + ): """Invokes Dapr's API for method invocation over HTTP. Args: diff --git a/dapr/clients/http/helpers.py b/dapr/clients/http/helpers.py index bf1e40c1..00d7a250 100644 --- a/dapr/clients/http/helpers.py +++ b/dapr/clients/http/helpers.py @@ -20,5 +20,6 @@ def get_api_url() -> str: if settings.DAPR_HTTP_ENDPOINT: return '{}/{}'.format(settings.DAPR_HTTP_ENDPOINT, settings.DAPR_API_VERSION) - return 'http://{}:{}/{}'.format(settings.DAPR_RUNTIME_HOST, settings.DAPR_HTTP_PORT, - settings.DAPR_API_VERSION) + return 'http://{}:{}/{}'.format( + settings.DAPR_RUNTIME_HOST, settings.DAPR_HTTP_PORT, settings.DAPR_API_VERSION + ) diff --git a/dapr/conf/helpers.py b/dapr/conf/helpers.py index aa004a1a..8740bde8 100644 --- a/dapr/conf/helpers.py +++ b/dapr/conf/helpers.py @@ -123,8 +123,11 @@ def _preprocess_uri(self, url: str) -> str: if len(url_list) == 3 and '://' not in url: # A URI like dns:mydomain:5000 or vsock:mycid:5000 was used url = url.replace(':', '://', 1) - elif (len(url_list) >= 2 and '://' not in url and url_list[ - 0] in URIParseConfig.ACCEPTED_SCHEMES): + elif ( + len(url_list) >= 2 + and '://' not in url + and url_list[0] in URIParseConfig.ACCEPTED_SCHEMES + ): # A URI like dns:mydomain or dns:[2001:db8:1f70::999:de8:7648:6e8]:mydomain was used # Possibly a URI like dns:[2001:db8:1f70::999:de8:7648:6e8]:mydomain was used url = url.replace(':', '://', 1) @@ -169,13 +172,18 @@ def tls(self) -> bool: def _validate_path_and_query(self) -> None: if self._parsed_url.path: raise ValueError( - f'paths are not supported for gRPC endpoints:' f" '{self._parsed_url.path}'") + f'paths are not supported for gRPC endpoints:' f" '{self._parsed_url.path}'" + ) if self._parsed_url.query: query_dict = parse_qs(self._parsed_url.query) if 'tls' in query_dict and self._parsed_url.scheme in ['http', 'https']: - raise ValueError(f'the tls query parameter is not supported for http(s) endpoints: ' - f"'{self._parsed_url.query}'") + raise ValueError( + f'the tls query parameter is not supported for http(s) endpoints: ' + f"'{self._parsed_url.query}'" + ) query_dict.pop('tls', None) if query_dict: - raise ValueError(f'query parameters are not supported for gRPC endpoints:' - f" '{self._parsed_url.query}'") + raise ValueError( + f'query parameters are not supported for gRPC endpoints:' + f" '{self._parsed_url.query}'" + ) diff --git a/examples/state_store/state_store.py b/examples/state_store/state_store.py index fa222b24..c8057c0a 100644 --- a/examples/state_store/state_store.py +++ b/examples/state_store/state_store.py @@ -40,19 +40,27 @@ def save_state(): # For detailed error messages from the dapr runtime: # print(f"Details={err.details()}) # Save multiple states. - d.save_bulk_state(store_name=storeName, - states=[StateItem(key=another_key, value=another_value), - StateItem(key=yet_another_key, value=yet_another_value), ], ) + d.save_bulk_state( + store_name=storeName, + states=[ + StateItem(key=another_key, value=another_value), + StateItem(key=yet_another_key, value=yet_another_value), + ], + ) print(f'State store has successfully saved {another_value} with {another_key} as key') print( - f'State store has successfully saved {yet_another_value} with {yet_another_key} as key') + f'State store has successfully saved {yet_another_value} with {yet_another_key} as key' + ) # Save bulk with etag that is different from the one stored in the database. try: - d.save_bulk_state(store_name=storeName, - states=[StateItem(key=another_key, value=another_value, etag='999'), - StateItem(key=yet_another_key, value=yet_another_value, - etag='999'), ], ) + d.save_bulk_state( + store_name=storeName, + states=[ + StateItem(key=another_key, value=another_value, etag='999'), + StateItem(key=yet_another_key, value=yet_another_value, etag='999'), + ], + ) except grpc.RpcError as err: # StatusCode should be StatusCode.ABORTED. print(f'Cannot save bulk due to bad etags. ErrorCode={err.code()}') @@ -64,14 +72,23 @@ def save_state(): print(f'Got value={state.data} eTag={state.etag}') # Transaction upsert - d.execute_state_transaction(store_name=storeName, operations=[ - TransactionalStateOperation(operation_type=TransactionOperationType.upsert, key=key, - data=updated_value, etag=state.etag, ), - TransactionalStateOperation(key=another_key, data=another_value), ], ) + d.execute_state_transaction( + store_name=storeName, + operations=[ + TransactionalStateOperation( + operation_type=TransactionOperationType.upsert, + key=key, + data=updated_value, + etag=state.etag, + ), + TransactionalStateOperation(key=another_key, data=another_value), + ], + ) # Batch get - items = d.get_bulk_state(store_name=storeName, keys=[key, another_key], - states_metadata={'metakey': 'metavalue'}).items + items = d.get_bulk_state( + store_name=storeName, keys=[key, another_key], states_metadata={'metakey': 'metavalue'} + ).items print(f'Got items with etags: {[(i.data, i.etag) for i in items]}') # Delete one state by key. From 3f6940bd61ef37147ca6b354567aabda0285dc5a Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Thu, 8 Feb 2024 15:32:17 +0000 Subject: [PATCH 06/29] Adds unit test Signed-off-by: Elena Kolevska --- dapr/clients/health.py | 8 +-- tests/clients/test_health_decorator.py | 67 ++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 tests/clients/test_health_decorator.py diff --git a/dapr/clients/health.py b/dapr/clients/health.py index b4f8743f..57d91bd5 100644 --- a/dapr/clients/health.py +++ b/dapr/clients/health.py @@ -42,15 +42,13 @@ async def async_wrapper(*args, **kwargs): try: async with session.get(healthz_url, headers=headers) as response: if 200 <= response.status < 300: - print('Dapr service is healthy.') break except aiohttp.ClientError as e: print(f'Health check failed: {e}') remaining = (start + timeout_s) - time.time() if remaining <= 0: - print('Health check timed out.') - return False + raise TimeoutError(f'Dapr health check timed out, after {timeout_s}.') await asyncio.sleep(min(1, remaining)) return await func(*args, **kwargs) @@ -60,15 +58,13 @@ def sync_wrapper(*args, **kwargs): req = urllib.request.Request(healthz_url, headers=headers) with urllib.request.urlopen(req) as response: if 200 <= response.status < 300: - print('Dapr service is healthy. SYNC') break except urllib.error.URLError as e: print(f'Health check failed: {e.reason}') remaining = (start + timeout_s) - time.time() if remaining <= 0: - print('Health check timed out.') - return False + raise TimeoutError(f'Dapr health check timed out, after {timeout_s}.') time.sleep(min(1, remaining)) return func(*args, **kwargs) diff --git a/tests/clients/test_health_decorator.py b/tests/clients/test_health_decorator.py new file mode 100644 index 00000000..f44f8ac7 --- /dev/null +++ b/tests/clients/test_health_decorator.py @@ -0,0 +1,67 @@ +import unittest +from unittest.mock import patch, MagicMock +import asyncio + +from dapr.clients.health import healthcheck + + +class TestHealthCheckDecorator(unittest.TestCase): + @patch('urllib.request.urlopen') + def test_healthcheck_sync(self, mock_urlopen): + # Mock the response to simulate a healthy Dapr service + mock_urlopen.return_value.__enter__.return_value = MagicMock(status=200) + + @healthcheck(timeout_s=1) + def sync_test_function(): + return 'Sync function executed' + + result = sync_test_function() + self.assertEqual(result, 'Sync function executed') + mock_urlopen.assert_called() + + @patch('urllib.request.urlopen') + def test_healthcheck_sync_unhealthy(self, mock_urlopen): + # Mock the response to simulate an unhealthy Dapr service + mock_urlopen.return_value.__enter__.return_value = MagicMock(status=500) + + @healthcheck(timeout_s=1) + def sync_test_function(): + return 'Sync function executed' + + with self.assertRaises(TimeoutError): + sync_test_function() + + mock_urlopen.assert_called() + + +class TestHealthCheckDecoratorAsync(unittest.IsolatedAsyncioTestCase): + @patch('aiohttp.ClientSession.get') + def test_healthcheck_async(self, mock_get): + # Mock the response to simulate a healthy Dapr service + mock_response = MagicMock(status=200) + mock_get.return_value.__aenter__.return_value = mock_response + + @healthcheck(timeout_s=1) + async def async_test_function(): + return 'Async function executed' + + async def run_test(): + result = await async_test_function() + self.assertEqual(result, 'Async function executed') + + asyncio.run(run_test()) + mock_get.assert_called() + + # @patch('aiohttp.ClientSession.get', new_callable=AsyncMock) + # async def test_healthcheck_async_unhealthy(self, mock_get): + # # Simulate an async timeout error + # mock_get.side_effect = asyncio.TimeoutError("Simulated async timeout") + # + # @healthcheck(timeout_s=1) + # async def async_test_function(): + # return "Async function executed" + # + # with self.assertRaises(ValueError) as context: + # await async_test_function() + # + # mock_get.assert_called() From 26292316c2c45501456bdcd92ff40d4f244d0f7c Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Thu, 8 Feb 2024 16:45:06 +0000 Subject: [PATCH 07/29] Repalces wait() with @healthcheck decorator in examples Signed-off-by: Elena Kolevska --- examples/README.md | 23 ++++--- examples/configuration/configuration.py | 12 ++-- examples/error_handling/error_handling.py | 69 ++++++++++--------- examples/state_store/state_store.py | 5 +- .../state_store_query/state_store_query.py | 37 +++++----- tests/clients/test_health_decorator.py | 29 ++++---- 6 files changed, 96 insertions(+), 79 deletions(-) diff --git a/examples/README.md b/examples/README.md index f2a26428..602c0069 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,18 +2,19 @@ These examples demonstrate how to use the Dapr Python SDK: -| Example | Description | -|---------|-------------| -| [Service invocation](./invoke-simple) | Invoke service by passing bytes data +| Example | Description | +|-------------------------------------------------------|-------------| +| [Service invocation](./invoke-simple) | Invoke service by passing bytes data | [Service invocation (advanced)](./invoke-custom-data) | Invoke service by using custom protobuf message -| [State management](./state_store) | Save and get state to/from the state store -| [Publish & subscribe](./pubsub-simple) | Publish and subscribe to events -| [Bindings](./invoke-binding) | Invoke an output binding to interact with external resources -| [Virtual actors](./demo_actor) | Try Dapr virtual actor features -| [Secrets](./secret_store) | Get secrets from a defined secret store -| [Distributed tracing](./w3c-tracing) | Leverage Dapr's built-in tracing support -| [Distributed lock](./distributed_lock) | Keep your application safe from race conditions by using distributed locks -| [Workflow](./demo_workflow) | Run a workflow to simulate an order processor +| [State management](./state_store) | Save and get state to/from the state store +| [Publish & subscribe](./pubsub-simple) | Publish and subscribe to events +| [Error handling](./error_handling) | Error handling +| [Bindings](./invoke-binding) | Invoke an output binding to interact with external resources +| [Virtual actors](./demo_actor) | Try Dapr virtual actor features +| [Secrets](./secret_store) | Get secrets from a defined secret store +| [Distributed tracing](./w3c-tracing) | Leverage Dapr's built-in tracing support +| [Distributed lock](./distributed_lock) | Keep your application safe from race conditions by using distributed locks +| [Workflow](./demo_workflow) | Run a workflow to simulate an order processor ## More information diff --git a/examples/configuration/configuration.py b/examples/configuration/configuration.py index c6e613fa..170306e2 100644 --- a/examples/configuration/configuration.py +++ b/examples/configuration/configuration.py @@ -6,6 +6,7 @@ from time import sleep from dapr.clients import DaprClient from dapr.clients.grpc._response import ConfigurationWatcher, ConfigurationResponse +from dapr.clients.health import healthcheck configuration: ConfigurationWatcher = ConfigurationWatcher() @@ -20,15 +21,13 @@ def handler(id: str, resp: ConfigurationResponse): ) +@healthcheck(timeout_s=10) async def executeConfiguration(): with DaprClient() as d: storeName = 'configurationstore' keys = ['orderId1', 'orderId2'] - # Wait for sidecar to be up within 20 seconds. - d.wait(20) - global configuration # Get one configuration by key. @@ -53,5 +52,10 @@ async def executeConfiguration(): isSuccess = d.unsubscribe_configuration(store_name=storeName, id=id) print(f'Unsubscribed successfully? {isSuccess}', flush=True) +try: + asyncio.run(executeConfiguration()) +except TimeoutError as e: + print("Dapr wasn't ready in time: {e}") +except Exception as e: + print(e) -asyncio.run(executeConfiguration()) diff --git a/examples/error_handling/error_handling.py b/examples/error_handling/error_handling.py index 94768d33..919f6937 100644 --- a/examples/error_handling/error_handling.py +++ b/examples/error_handling/error_handling.py @@ -1,41 +1,42 @@ from dapr.clients import DaprClient from dapr.clients.exceptions import DaprGrpcError +from dapr.clients.health import healthcheck -with DaprClient() as d: - storeName = 'statestore' - key = 'key||' - value = 'value_1' +@healthcheck(5) +def run_example(): + with DaprClient() as d: + storeName = 'statestore' - # Wait for sidecar to be up within 5 seconds. - d.wait(5) + key = 'key||' + value = 'value_1' - # Save single state. - try: - d.save_state(store_name=storeName, key=key, value=value) - except DaprGrpcError as err: - print(f'Status code: {err.code()}', flush=True) - print(f'Message: {err.details()}', flush=True) - print(f'Error code: {err.error_code()}', flush=True) + # Save single state. + try: + d.save_state(store_name=storeName, key=key, value=value) + except DaprGrpcError as err: + print(f'Status code: {err.code()}', flush=True) + print(f'Message: {err.details()}', flush=True) + print(f'Error code: {err.error_code()}', flush=True) - if err.status_details().error_info is not None: - print(f'Error info(reason): {err.status_details().error_info["reason"]}', flush=True) - if err.status_details().resource_info is not None: - print( - f'Resource info (resource type): {err.status_details().resource_info["resource_type"]}', - flush=True, - ) - print( - f'Resource info (resource name): {err.status_details().resource_info["resource_name"]}', - flush=True, - ) - if err.status_details().bad_request is not None: - print( - f'Bad request (field): {err.status_details().bad_request["field_violations"][0]["field"]}', - flush=True, - ) - print( - f'Bad request (description): {err.status_details().bad_request["field_violations"][0]["description"]}', - flush=True, - ) - print(f'JSON: {err.json()}', flush=True) + if err.status_details().error_info is not None: + print(f'Error info(reason): {err.status_details().error_info["reason"]}', + flush=True) + if err.status_details().resource_info is not None: + print( + f'Resource info (resource type): {err.status_details().resource_info["resource_type"]}', + flush=True, ) + print( + f'Resource info (resource name): {err.status_details().resource_info["resource_name"]}', + flush=True, ) + if err.status_details().bad_request is not None: + print( + f'Bad request (field): {err.status_details().bad_request["field_violations"][0]["field"]}', + flush=True, ) + print( + f'Bad request (description): {err.status_details().bad_request["field_violations"][0]["description"]}', + flush=True, ) + print(f'JSON: {err.json()}', flush=True) + + +run_example() diff --git a/examples/state_store/state_store.py b/examples/state_store/state_store.py index c8057c0a..56ec942f 100644 --- a/examples/state_store/state_store.py +++ b/examples/state_store/state_store.py @@ -97,4 +97,7 @@ def save_state(): print(f'Got value after delete: {data}') -save_state() +try: + save_state() +except TimeoutError as e: + print(f'Error: {e}') diff --git a/examples/state_store_query/state_store_query.py b/examples/state_store_query/state_store_query.py index 90aa23be..887ffae9 100644 --- a/examples/state_store_query/state_store_query.py +++ b/examples/state_store_query/state_store_query.py @@ -7,24 +7,29 @@ import json -with DaprClient() as d: - storeName = 'statestore' +from dapr.clients.health import healthcheck - # Wait for sidecar to be up within 5 seconds. - d.wait(5) - # Query the state store +@healthcheck(5) +def state_store_query(): + with DaprClient() as d: + store_name = 'statestore' - query = open('query.json', 'r').read() - res = d.query_state(store_name=storeName, query=query) - for r in res.results: - print(r.key, json.dumps(json.loads(str(r.value, 'UTF-8')), sort_keys=True)) - print('Token:', res.token) + # Query the state store - # Get more results using a pagination token + query = open('query.json', 'r').read() + res = d.query_state(store_name=store_name, query=query) + for r in res.results: + print(r.key, json.dumps(json.loads(str(r.value, 'UTF-8')), sort_keys=True)) + print('Token:', res.token) - query = open('query-token.json', 'r').read() - res = d.query_state(store_name=storeName, query=query) - for r in res.results: - print(r.key, json.dumps(json.loads(str(r.value, 'UTF-8')), sort_keys=True)) - print('Token:', res.token) + # Get more results using a pagination token + + query = open('query-token.json', 'r').read() + res = d.query_state(store_name=store_name, query=query) + for r in res.results: + print(r.key, json.dumps(json.loads(str(r.value, 'UTF-8')), sort_keys=True)) + print('Token:', res.token) + + +state_store_query() \ No newline at end of file diff --git a/tests/clients/test_health_decorator.py b/tests/clients/test_health_decorator.py index f44f8ac7..01561651 100644 --- a/tests/clients/test_health_decorator.py +++ b/tests/clients/test_health_decorator.py @@ -52,16 +52,19 @@ async def run_test(): asyncio.run(run_test()) mock_get.assert_called() - # @patch('aiohttp.ClientSession.get', new_callable=AsyncMock) - # async def test_healthcheck_async_unhealthy(self, mock_get): - # # Simulate an async timeout error - # mock_get.side_effect = asyncio.TimeoutError("Simulated async timeout") - # - # @healthcheck(timeout_s=1) - # async def async_test_function(): - # return "Async function executed" - # - # with self.assertRaises(ValueError) as context: - # await async_test_function() - # - # mock_get.assert_called() + @patch('aiohttp.ClientSession.get') + def test_healthcheck_async_unhealthy(self, mock_get): + # Mock the response to simulate an unhealthy Dapr service + mock_response = MagicMock(status=500) + mock_get.return_value.__aenter__.return_value = mock_response + + @healthcheck(timeout_s=1) + async def async_test_function(): + return 'Async function executed' + + async def run_test(): + with self.assertRaises(TimeoutError) as context: + await async_test_function() + + asyncio.run(run_test()) + mock_get.assert_called() From 8bcec71aa6f36d78bee64f9ad649953d6b38d29c Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Thu, 8 Feb 2024 16:47:25 +0000 Subject: [PATCH 08/29] Ruff Signed-off-by: Elena Kolevska --- examples/configuration/configuration.py | 2 +- examples/error_handling/error_handling.py | 17 +++++++++++------ examples/state_store_query/state_store_query.py | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/examples/configuration/configuration.py b/examples/configuration/configuration.py index 170306e2..7162acb4 100644 --- a/examples/configuration/configuration.py +++ b/examples/configuration/configuration.py @@ -52,10 +52,10 @@ async def executeConfiguration(): isSuccess = d.unsubscribe_configuration(store_name=storeName, id=id) print(f'Unsubscribed successfully? {isSuccess}', flush=True) + try: asyncio.run(executeConfiguration()) except TimeoutError as e: print("Dapr wasn't ready in time: {e}") except Exception as e: print(e) - diff --git a/examples/error_handling/error_handling.py b/examples/error_handling/error_handling.py index 919f6937..3e2f3199 100644 --- a/examples/error_handling/error_handling.py +++ b/examples/error_handling/error_handling.py @@ -20,22 +20,27 @@ def run_example(): print(f'Error code: {err.error_code()}', flush=True) if err.status_details().error_info is not None: - print(f'Error info(reason): {err.status_details().error_info["reason"]}', - flush=True) + print( + f'Error info(reason): {err.status_details().error_info["reason"]}', flush=True + ) if err.status_details().resource_info is not None: print( f'Resource info (resource type): {err.status_details().resource_info["resource_type"]}', - flush=True, ) + flush=True, + ) print( f'Resource info (resource name): {err.status_details().resource_info["resource_name"]}', - flush=True, ) + flush=True, + ) if err.status_details().bad_request is not None: print( f'Bad request (field): {err.status_details().bad_request["field_violations"][0]["field"]}', - flush=True, ) + flush=True, + ) print( f'Bad request (description): {err.status_details().bad_request["field_violations"][0]["description"]}', - flush=True, ) + flush=True, + ) print(f'JSON: {err.json()}', flush=True) diff --git a/examples/state_store_query/state_store_query.py b/examples/state_store_query/state_store_query.py index 887ffae9..994e2d57 100644 --- a/examples/state_store_query/state_store_query.py +++ b/examples/state_store_query/state_store_query.py @@ -32,4 +32,4 @@ def state_store_query(): print('Token:', res.token) -state_store_query() \ No newline at end of file +state_store_query() From 3a208a2153fc906dfe2ad8d138bd8622a7bf932a Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Thu, 8 Feb 2024 16:50:48 +0000 Subject: [PATCH 09/29] Linter Signed-off-by: Elena Kolevska --- tests/clients/test_health_decorator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/clients/test_health_decorator.py b/tests/clients/test_health_decorator.py index 01561651..3d480ad2 100644 --- a/tests/clients/test_health_decorator.py +++ b/tests/clients/test_health_decorator.py @@ -63,7 +63,7 @@ async def async_test_function(): return 'Async function executed' async def run_test(): - with self.assertRaises(TimeoutError) as context: + with self.assertRaises(TimeoutError): await async_test_function() asyncio.run(run_test()) From 5bd279c51e3eac6bd07bdededebe14441f820be3 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Fri, 9 Feb 2024 17:14:18 +0000 Subject: [PATCH 10/29] updates heathcheck decorator to use a global var Signed-off-by: Elena Kolevska --- dapr/clients/__init__.py | 4 +- dapr/clients/grpc/client.py | 2 + dapr/clients/health.py | 20 ++++--- dapr/clients/http/client.py | 11 ++-- dapr/clients/http/conf.py | 21 +++++++ .../http/dapr_invocation_http_client.py | 3 +- dapr/clients/http/helpers.py | 5 +- examples/configuration/configuration.py | 2 +- tests/actor/test_actor.py | 5 ++ tests/actor/test_actor_reentrancy.py | 5 ++ tests/clients/fake_dapr_server.py | 6 ++ tests/clients/fake_http_server.py | 3 + tests/clients/test_health_decorator.py | 60 +++++++++++++++++-- 13 files changed, 122 insertions(+), 25 deletions(-) create mode 100644 dapr/clients/http/conf.py diff --git a/dapr/clients/__init__.py b/dapr/clients/__init__.py index 042f0dd4..0bf383cb 100644 --- a/dapr/clients/__init__.py +++ b/dapr/clients/__init__.py @@ -24,14 +24,16 @@ from dapr.conf import settings from google.protobuf.message import Message as GrpcMessage + __all__ = [ 'DaprClient', 'DaprActorClientBase', 'DaprActorHttpClient', 'DaprInternalError', - 'ERROR_CODE_UNKNOWN', + 'ERROR_CODE_UNKNOWN' ] + from grpc import ( # type: ignore UnaryUnaryClientInterceptor, UnaryStreamClientInterceptor, diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index 2fb69b71..f6a4927a 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -40,6 +40,7 @@ from dapr.clients.exceptions import DaprInternalError, DaprGrpcError from dapr.clients.grpc._state import StateOptions, StateItem from dapr.clients.grpc._helpers import getWorkflowRuntimeStatus +from dapr.clients.health import healthcheck from dapr.conf import settings from dapr.proto import api_v1, api_service_v1, common_v1 from dapr.proto.runtime.v1.dapr_pb2 import UnsubscribeConfigurationResponse @@ -101,6 +102,7 @@ class DaprGrpcClient: ... resp = d.invoke_method('callee', 'method', b'data') """ + @healthcheck(5) def __init__( self, address: Optional[str] = None, diff --git a/dapr/clients/health.py b/dapr/clients/health.py index 57d91bd5..72d33eb7 100644 --- a/dapr/clients/health.py +++ b/dapr/clients/health.py @@ -21,14 +21,16 @@ import aiohttp -from dapr.clients.http.client import DAPR_API_TOKEN_HEADER, USER_AGENT_HEADER, DAPR_USER_AGENT +from dapr.clients.http.conf import DAPR_API_TOKEN_HEADER, USER_AGENT_HEADER, DAPR_USER_AGENT from dapr.clients.http.helpers import get_api_url from dapr.conf import settings +HEALTHY = False + def healthcheck(timeout_s: int = 5): def decorator(func): - healthz_url = f'{get_api_url()}/healthz/outbound' + health_url = f'{get_api_url()}/healthz/outbound' headers = {USER_AGENT_HEADER: DAPR_USER_AGENT} if settings.DAPR_API_TOKEN is not None: headers[DAPR_API_TOKEN_HEADER] = settings.DAPR_API_TOKEN @@ -38,10 +40,12 @@ def decorator(func): async def async_wrapper(*args, **kwargs): # Async health check logic async with aiohttp.ClientSession() as session: - while True: + global HEALTHY + while not HEALTHY: try: - async with session.get(healthz_url, headers=headers) as response: + async with session.get(health_url, headers=headers) as response: if 200 <= response.status < 300: + HEALTHY = True break except aiohttp.ClientError as e: print(f'Health check failed: {e}') @@ -53,14 +57,16 @@ async def async_wrapper(*args, **kwargs): return await func(*args, **kwargs) def sync_wrapper(*args, **kwargs): - while True: + global HEALTHY + while not HEALTHY: try: - req = urllib.request.Request(healthz_url, headers=headers) + req = urllib.request.Request(health_url, headers=headers) with urllib.request.urlopen(req) as response: if 200 <= response.status < 300: + HEALTHY = True break except urllib.error.URLError as e: - print(f'Health check failed: {e.reason}') + print(f'Health check on {health_url} failed: {e.reason}') remaining = (start + timeout_s) - time.time() if remaining <= 0: diff --git a/dapr/clients/http/client.py b/dapr/clients/http/client.py index e5a12e11..123c8c3b 100644 --- a/dapr/clients/http/client.py +++ b/dapr/clients/http/client.py @@ -17,23 +17,22 @@ from typing import Callable, Mapping, Dict, Optional, Union, Tuple, TYPE_CHECKING +from dapr.clients.http.conf import DAPR_API_TOKEN_HEADER, USER_AGENT_HEADER, DAPR_USER_AGENT, \ + CONTENT_TYPE_HEADER +from dapr.clients.health import healthcheck + if TYPE_CHECKING: from dapr.serializers import Serializer from dapr.conf import settings from dapr.clients.base import DEFAULT_JSON_CONTENT_TYPE from dapr.clients.exceptions import DaprInternalError, ERROR_CODE_DOES_NOT_EXIST, ERROR_CODE_UNKNOWN -from dapr.version import __version__ - -CONTENT_TYPE_HEADER = 'content-type' -DAPR_API_TOKEN_HEADER = 'dapr-api-token' -USER_AGENT_HEADER = 'User-Agent' -DAPR_USER_AGENT = f'dapr-sdk-python/{__version__}' class DaprHttpClient: """A Dapr Http API client""" + @healthcheck(5) def __init__( self, message_serializer: 'Serializer', diff --git a/dapr/clients/http/conf.py b/dapr/clients/http/conf.py new file mode 100644 index 00000000..2dce2834 --- /dev/null +++ b/dapr/clients/http/conf.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2023 The Dapr Authors +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. +""" + +from dapr.version import __version__ + +CONTENT_TYPE_HEADER = 'content-type' +DAPR_API_TOKEN_HEADER = 'dapr-api-token' +USER_AGENT_HEADER = 'User-Agent' +DAPR_USER_AGENT = f'dapr-sdk-python/{__version__}' diff --git a/dapr/clients/http/dapr_invocation_http_client.py b/dapr/clients/http/dapr_invocation_http_client.py index cbffb34e..ca1a5dfa 100644 --- a/dapr/clients/http/dapr_invocation_http_client.py +++ b/dapr/clients/http/dapr_invocation_http_client.py @@ -18,9 +18,10 @@ from typing import Callable, Dict, Optional, Union from multidict import MultiDict -from dapr.clients.http.client import DaprHttpClient, CONTENT_TYPE_HEADER +from dapr.clients.http.client import DaprHttpClient from dapr.clients.grpc._helpers import MetadataTuple, GrpcMessage from dapr.clients.grpc._response import InvokeMethodResponse +from dapr.clients.http.conf import CONTENT_TYPE_HEADER from dapr.clients.http.helpers import get_api_url from dapr.serializers import DefaultJSONSerializer from dapr.version import __version__ diff --git a/dapr/clients/http/helpers.py b/dapr/clients/http/helpers.py index 00d7a250..c58eff45 100644 --- a/dapr/clients/http/helpers.py +++ b/dapr/clients/http/helpers.py @@ -20,6 +20,5 @@ def get_api_url() -> str: if settings.DAPR_HTTP_ENDPOINT: return '{}/{}'.format(settings.DAPR_HTTP_ENDPOINT, settings.DAPR_API_VERSION) - return 'http://{}:{}/{}'.format( - settings.DAPR_RUNTIME_HOST, settings.DAPR_HTTP_PORT, settings.DAPR_API_VERSION - ) + return 'http://{}:{}/{}'.format(settings.DAPR_RUNTIME_HOST, settings.DAPR_HTTP_PORT, + settings.DAPR_API_VERSION) diff --git a/examples/configuration/configuration.py b/examples/configuration/configuration.py index 7162acb4..9c13a088 100644 --- a/examples/configuration/configuration.py +++ b/examples/configuration/configuration.py @@ -21,7 +21,7 @@ def handler(id: str, resp: ConfigurationResponse): ) -@healthcheck(timeout_s=10) +@healthcheck(5) async def executeConfiguration(): with DaprClient() as d: storeName = 'configurationstore' diff --git a/tests/actor/test_actor.py b/tests/actor/test_actor.py index d8b209c0..e6d197fc 100644 --- a/tests/actor/test_actor.py +++ b/tests/actor/test_actor.py @@ -23,6 +23,7 @@ from dapr.actor.runtime.context import ActorRuntimeContext from dapr.actor.runtime.runtime import ActorRuntime from dapr.actor.runtime._type_information import ActorTypeInformation +from dapr.clients.health import health from dapr.serializers import DefaultJSONSerializer from tests.actor.fake_actor_classes import ( @@ -39,12 +40,16 @@ class ActorTests(unittest.TestCase): def setUp(self): + health.HEALTHY = True ActorRuntime._actor_managers = {} ActorRuntime.set_actor_config(ActorRuntimeConfig()) self._serializer = DefaultJSONSerializer() _run(ActorRuntime.register_actor(FakeSimpleActor)) _run(ActorRuntime.register_actor(FakeMultiInterfacesActor)) + def tearDown(self): + health.HEALTHY = False + def test_get_registered_actor_types(self): actor_types = ActorRuntime.get_registered_actor_types() self.assertTrue(actor_types.index('FakeSimpleActor') >= 0) diff --git a/tests/actor/test_actor_reentrancy.py b/tests/actor/test_actor_reentrancy.py index 40b948a5..ad46948f 100644 --- a/tests/actor/test_actor_reentrancy.py +++ b/tests/actor/test_actor_reentrancy.py @@ -20,6 +20,7 @@ from dapr.actor.runtime.runtime import ActorRuntime from dapr.actor.runtime.config import ActorRuntimeConfig, ActorReentrancyConfig +from dapr.clients import health from dapr.serializers import DefaultJSONSerializer from tests.actor.fake_actor_classes import ( @@ -33,6 +34,7 @@ class ActorRuntimeTests(unittest.TestCase): def setUp(self): + health.HEALTHY = True ActorRuntime._actor_managers = {} ActorRuntime.set_actor_config( ActorRuntimeConfig(reentrancy=ActorReentrancyConfig(enabled=True)) @@ -42,6 +44,9 @@ def setUp(self): _run(ActorRuntime.register_actor(FakeSlowReentrantActor)) _run(ActorRuntime.register_actor(FakeMultiInterfacesActor)) + def tearDown(self): + health.HEALTHY = False + def test_reentrant_dispatch(self): _run(ActorRuntime.register_actor(FakeMultiInterfacesActor)) diff --git a/tests/clients/fake_dapr_server.py b/tests/clients/fake_dapr_server.py index f4e6bd81..b63a9cf2 100644 --- a/tests/clients/fake_dapr_server.py +++ b/tests/clients/fake_dapr_server.py @@ -5,6 +5,8 @@ from google.protobuf.any_pb2 import Any as GrpcAny from google.protobuf import empty_pb2 from grpc_status import rpc_status + +from dapr.clients import health from dapr.clients.grpc._helpers import to_bytes from dapr.proto import api_service_v1, common_v1, api_v1 from dapr.proto.common.v1.common_pb2 import ConfigurationItem @@ -52,10 +54,12 @@ def __init__(self): self._next_exception = None def start(self, port: int = 8080): + health.HEALTHY = True self._server.add_insecure_port(f'[::]:{port}') self._server.start() def start_secure(self, port: int = 4443): + health.HEALTHY = True create_certificates() private_key_file = open(PRIVATE_KEY_PATH, 'rb') @@ -75,9 +79,11 @@ def start_secure(self, port: int = 4443): self._server.start() def stop(self): + health.HEALTHY = False self._server.stop(None) def stop_secure(self): + health.HEALTHY = False self._server.stop(None) delete_certificates() diff --git a/tests/clients/fake_http_server.py b/tests/clients/fake_http_server.py index e019b3ff..c5bd15d1 100644 --- a/tests/clients/fake_http_server.py +++ b/tests/clients/fake_http_server.py @@ -4,6 +4,7 @@ from threading import Thread from http.server import BaseHTTPRequestHandler, HTTPServer +from dapr.clients import health from tests.clients.certs import ( CERTIFICATE_CHAIN_PATH, PRIVATE_KEY_PATH, @@ -82,6 +83,7 @@ def get_request_headers(self): return self.server.request_headers def shutdown_server(self): + health.HEALTHY = False self.server.shutdown() self.server.socket.close() self.join() @@ -102,4 +104,5 @@ def set_server_delay(self, delay_seconds): self.server.sleep_time = delay_seconds def run(self): + health.HEALTHY = True self.server.serve_forever() diff --git a/tests/clients/test_health_decorator.py b/tests/clients/test_health_decorator.py index 3d480ad2..274b69de 100644 --- a/tests/clients/test_health_decorator.py +++ b/tests/clients/test_health_decorator.py @@ -1,17 +1,24 @@ +import asyncio import unittest from unittest.mock import patch, MagicMock -import asyncio -from dapr.clients.health import healthcheck +from dapr.clients import health class TestHealthCheckDecorator(unittest.TestCase): + + def tearDown(self): + # Reset the global var to true, because it's needed for other tests + health.HEALTHY = True + @patch('urllib.request.urlopen') def test_healthcheck_sync(self, mock_urlopen): # Mock the response to simulate a healthy Dapr service mock_urlopen.return_value.__enter__.return_value = MagicMock(status=200) - @healthcheck(timeout_s=1) + health.HEALTHY = False + + @health.healthcheck(1) def sync_test_function(): return 'Sync function executed' @@ -22,9 +29,12 @@ def sync_test_function(): @patch('urllib.request.urlopen') def test_healthcheck_sync_unhealthy(self, mock_urlopen): # Mock the response to simulate an unhealthy Dapr service + mock_urlopen.return_value.__enter__.return_value = MagicMock(status=500) - @healthcheck(timeout_s=1) + health.HEALTHY = False + + @health.healthcheck(1) def sync_test_function(): return 'Sync function executed' @@ -33,6 +43,22 @@ def sync_test_function(): mock_urlopen.assert_called() + @patch('urllib.request.urlopen') + def test_healthcheck_sync_unhealthy_with_global_healthy(self, mock_urlopen): + # Mock the response to simulate an unhealthy Dapr service + mock_urlopen.return_value.__enter__.return_value = MagicMock(status=500) + + health.HEALTHY = True + + @health.healthcheck(1) + def sync_test_function(): + return 'Sync function executed' + + sync_test_function() + + # Assert we never called the health endpoint, because the global var has already been set + mock_urlopen.assert_not_called() + class TestHealthCheckDecoratorAsync(unittest.IsolatedAsyncioTestCase): @patch('aiohttp.ClientSession.get') @@ -41,7 +67,9 @@ def test_healthcheck_async(self, mock_get): mock_response = MagicMock(status=200) mock_get.return_value.__aenter__.return_value = mock_response - @healthcheck(timeout_s=1) + health.HEALTHY = False + + @health.healthcheck(1) async def async_test_function(): return 'Async function executed' @@ -58,7 +86,9 @@ def test_healthcheck_async_unhealthy(self, mock_get): mock_response = MagicMock(status=500) mock_get.return_value.__aenter__.return_value = mock_response - @healthcheck(timeout_s=1) + health.HEALTHY = False + + @health.healthcheck(1) async def async_test_function(): return 'Async function executed' @@ -68,3 +98,21 @@ async def run_test(): asyncio.run(run_test()) mock_get.assert_called() + + @patch('aiohttp.ClientSession.get') + def test_healthcheck_async_unhealthy_with_global(self, mock_get): + # Mock the response to simulate an unhealthy Dapr service + mock_response = MagicMock(status=500) + mock_get.return_value.__aenter__.return_value = mock_response + + health.HEALTHY = True + + @health.healthcheck(1) + async def async_test_function(): + return 'Async function executed' + + async def run_test(): + await async_test_function() + + asyncio.run(run_test()) + mock_get.assert_not_called() From b12beba2b8844276084ac38a0f3231f734840d82 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Fri, 9 Feb 2024 18:16:06 +0000 Subject: [PATCH 11/29] Fixes tests Signed-off-by: Elena Kolevska --- dapr/clients/__init__.py | 2 +- dapr/clients/http/client.py | 8 +++++-- dapr/clients/http/helpers.py | 5 ++-- ext/dapr-ext-fastapi/tests/test_app.py | 11 ++++++--- ext/dapr-ext-fastapi/tests/test_dapractor.py | 10 ++++++++ ext/flask_dapr/tests/test_app.py | 12 +++++++--- tests/actor/test_actor.py | 23 +++++++++++-------- tests/actor/test_actor_runtime.py | 5 ++++ tests/actor/test_client_proxy.py | 24 ++++++++++++++------ tests/clients/test_health_decorator.py | 1 - 10 files changed, 72 insertions(+), 29 deletions(-) diff --git a/dapr/clients/__init__.py b/dapr/clients/__init__.py index 0bf383cb..b39124b0 100644 --- a/dapr/clients/__init__.py +++ b/dapr/clients/__init__.py @@ -30,7 +30,7 @@ 'DaprActorClientBase', 'DaprActorHttpClient', 'DaprInternalError', - 'ERROR_CODE_UNKNOWN' + 'ERROR_CODE_UNKNOWN', ] diff --git a/dapr/clients/http/client.py b/dapr/clients/http/client.py index 123c8c3b..a00b8ed0 100644 --- a/dapr/clients/http/client.py +++ b/dapr/clients/http/client.py @@ -17,8 +17,12 @@ from typing import Callable, Mapping, Dict, Optional, Union, Tuple, TYPE_CHECKING -from dapr.clients.http.conf import DAPR_API_TOKEN_HEADER, USER_AGENT_HEADER, DAPR_USER_AGENT, \ - CONTENT_TYPE_HEADER +from dapr.clients.http.conf import ( + DAPR_API_TOKEN_HEADER, + USER_AGENT_HEADER, + DAPR_USER_AGENT, + CONTENT_TYPE_HEADER, +) from dapr.clients.health import healthcheck if TYPE_CHECKING: diff --git a/dapr/clients/http/helpers.py b/dapr/clients/http/helpers.py index c58eff45..00d7a250 100644 --- a/dapr/clients/http/helpers.py +++ b/dapr/clients/http/helpers.py @@ -20,5 +20,6 @@ def get_api_url() -> str: if settings.DAPR_HTTP_ENDPOINT: return '{}/{}'.format(settings.DAPR_HTTP_ENDPOINT, settings.DAPR_API_VERSION) - return 'http://{}:{}/{}'.format(settings.DAPR_RUNTIME_HOST, settings.DAPR_HTTP_PORT, - settings.DAPR_API_VERSION) + return 'http://{}:{}/{}'.format( + settings.DAPR_RUNTIME_HOST, settings.DAPR_HTTP_PORT, settings.DAPR_API_VERSION + ) diff --git a/ext/dapr-ext-fastapi/tests/test_app.py b/ext/dapr-ext-fastapi/tests/test_app.py index 98da225b..0ed98d12 100644 --- a/ext/dapr-ext-fastapi/tests/test_app.py +++ b/ext/dapr-ext-fastapi/tests/test_app.py @@ -1,9 +1,14 @@ +import unittest + from fastapi import FastAPI -from fastapi.testclient import TestClient -from dapr.ext.fastapi import DaprApp from pydantic import BaseModel +from fastapi.testclient import TestClient -import unittest +from dapr.clients import health + +health.HEALTHY = True + +from dapr.ext.fastapi import DaprApp # noqa: E402 class Message(BaseModel): diff --git a/ext/dapr-ext-fastapi/tests/test_dapractor.py b/ext/dapr-ext-fastapi/tests/test_dapractor.py index ee863d72..dc363182 100644 --- a/ext/dapr-ext-fastapi/tests/test_dapractor.py +++ b/ext/dapr-ext-fastapi/tests/test_dapractor.py @@ -20,8 +20,18 @@ from dapr.ext.fastapi.actor import DaprActor, _wrap_response +from dapr.clients import health + +health.HEALTHY = True + class DaprActorTest(unittest.TestCase): + def setUp(self): + health.HEALTHY = True + + def tearDown(self): + health.HEALTHY = False + def test_wrap_response_str(self): r = _wrap_response(200, 'fake_message') self.assertEqual({'message': 'fake_message'}, json.loads(r.body)) diff --git a/ext/flask_dapr/tests/test_app.py b/ext/flask_dapr/tests/test_app.py index 8fb764a5..7ffa9a22 100644 --- a/ext/flask_dapr/tests/test_app.py +++ b/ext/flask_dapr/tests/test_app.py @@ -1,9 +1,12 @@ +import unittest from flask import Flask -from flask_dapr import DaprApp +import json -import unittest +from dapr.clients import health -import json +health.HEALTHY = True + +from flask_dapr import DaprApp # noqa: E402 class DaprAppTest(unittest.TestCase): @@ -13,6 +16,9 @@ def setUp(self): self.dapr_app = DaprApp(self.app) self.client = self.app.test_client() + def tearDown(self): + health.HEALTHY = False + def test_subscribe_subscription_registered(self): @self.dapr_app.subscribe(pubsub='pubsub', topic='test') def event_handler(): diff --git a/tests/actor/test_actor.py b/tests/actor/test_actor.py index e6d197fc..7d356805 100644 --- a/tests/actor/test_actor.py +++ b/tests/actor/test_actor.py @@ -18,24 +18,27 @@ from unittest import mock from datetime import timedelta -from dapr.actor.id import ActorId -from dapr.actor.runtime.config import ActorRuntimeConfig -from dapr.actor.runtime.context import ActorRuntimeContext -from dapr.actor.runtime.runtime import ActorRuntime -from dapr.actor.runtime._type_information import ActorTypeInformation -from dapr.clients.health import health -from dapr.serializers import DefaultJSONSerializer +from dapr.clients import health + +health.HEALTHY = True + +from dapr.actor.id import ActorId # noqa: E402 +from dapr.actor.runtime.config import ActorRuntimeConfig # noqa: E402 +from dapr.actor.runtime.context import ActorRuntimeContext # noqa: E402 +from dapr.actor.runtime.runtime import ActorRuntime # noqa: E402 +from dapr.actor.runtime._type_information import ActorTypeInformation # noqa: E402 +from dapr.serializers import DefaultJSONSerializer # noqa: E402 from tests.actor.fake_actor_classes import ( FakeSimpleActor, FakeSimpleReminderActor, FakeSimpleTimerActor, FakeMultiInterfacesActor, -) +) # noqa: E402 -from tests.actor.fake_client import FakeDaprActorClient +from tests.actor.fake_client import FakeDaprActorClient # noqa: E402 -from tests.actor.utils import _async_mock, _run +from tests.actor.utils import _async_mock, _run # noqa: E402 class ActorTests(unittest.TestCase): diff --git a/tests/actor/test_actor_runtime.py b/tests/actor/test_actor_runtime.py index 3aa24289..a480da7d 100644 --- a/tests/actor/test_actor_runtime.py +++ b/tests/actor/test_actor_runtime.py @@ -19,6 +19,7 @@ from dapr.actor.runtime.runtime import ActorRuntime from dapr.actor.runtime.config import ActorRuntimeConfig +from dapr.clients import health from dapr.serializers import DefaultJSONSerializer from tests.actor.fake_actor_classes import ( @@ -32,6 +33,7 @@ class ActorRuntimeTests(unittest.TestCase): def setUp(self): + health.HEALTHY = True ActorRuntime._actor_managers = {} ActorRuntime.set_actor_config(ActorRuntimeConfig()) self._serializer = DefaultJSONSerializer() @@ -39,6 +41,9 @@ def setUp(self): _run(ActorRuntime.register_actor(FakeMultiInterfacesActor)) _run(ActorRuntime.register_actor(FakeSimpleTimerActor)) + def tearDown(self): + health.HEALTHY = False + def test_get_registered_actor_types(self): actor_types = ActorRuntime.get_registered_actor_types() self.assertTrue(actor_types.index('FakeSimpleActor') >= 0) diff --git a/tests/actor/test_client_proxy.py b/tests/actor/test_client_proxy.py index 2e21b634..0dd051f9 100644 --- a/tests/actor/test_client_proxy.py +++ b/tests/actor/test_client_proxy.py @@ -12,23 +12,28 @@ See the License for the specific language governing permissions and limitations under the License. """ - import unittest from unittest import mock -from dapr.actor.id import ActorId -from dapr.actor.client.proxy import ActorProxy -from dapr.serializers import DefaultJSONSerializer +from dapr.clients import health + +# Set the HEALTHY variable right after importing the health module +health.HEALTHY = True + +from dapr.actor.id import ActorId # noqa: E402 +from dapr.actor.client.proxy import ActorProxy # noqa: E402 +from dapr.serializers import DefaultJSONSerializer # noqa: E402 from tests.actor.fake_actor_classes import ( FakeMultiInterfacesActor, FakeActorCls2Interface, -) +) # noqa: E402 -from tests.actor.fake_client import FakeDaprActorClient -from tests.actor.utils import _async_mock, _run +from tests.actor.fake_client import FakeDaprActorClient # noqa: E402 + +from tests.actor.utils import _async_mock, _run # noqa: E402 class FakeActoryProxyFactory: @@ -44,6 +49,8 @@ def create(self, actor_interface, actor_type, actor_id) -> ActorProxy: class ActorProxyTests(unittest.TestCase): def setUp(self): + print('\nStarting test ..') + health.HEALTHY = True # Create mock client self._fake_client = FakeDaprActorClient self._fake_factory = FakeActoryProxyFactory(self._fake_client) @@ -54,6 +61,9 @@ def setUp(self): self._fake_factory, ) + def tearDown(self): + health.HEALTHY = False + @mock.patch( 'tests.actor.fake_client.FakeDaprActorClient.invoke_method', new=_async_mock(return_value=b'"expected_response"'), diff --git a/tests/clients/test_health_decorator.py b/tests/clients/test_health_decorator.py index 274b69de..a2f4649d 100644 --- a/tests/clients/test_health_decorator.py +++ b/tests/clients/test_health_decorator.py @@ -6,7 +6,6 @@ class TestHealthCheckDecorator(unittest.TestCase): - def tearDown(self): # Reset the global var to true, because it's needed for other tests health.HEALTHY = True From d39652d6440a562cfdd579e597dacbd2a4669dff Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Fri, 9 Feb 2024 18:41:25 +0000 Subject: [PATCH 12/29] Linter fixes Signed-off-by: Elena Kolevska --- tests/actor/test_actor.py | 5 ++--- tests/actor/test_client_proxy.py | 4 ++-- .../test_secure_http_service_invocation_client.py | 11 ----------- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/tests/actor/test_actor.py b/tests/actor/test_actor.py index 7d356805..93eea9a9 100644 --- a/tests/actor/test_actor.py +++ b/tests/actor/test_actor.py @@ -29,15 +29,14 @@ from dapr.actor.runtime._type_information import ActorTypeInformation # noqa: E402 from dapr.serializers import DefaultJSONSerializer # noqa: E402 -from tests.actor.fake_actor_classes import ( +from tests.actor.fake_actor_classes import ( # noqa: E402 FakeSimpleActor, FakeSimpleReminderActor, FakeSimpleTimerActor, FakeMultiInterfacesActor, -) # noqa: E402 +) from tests.actor.fake_client import FakeDaprActorClient # noqa: E402 - from tests.actor.utils import _async_mock, _run # noqa: E402 diff --git a/tests/actor/test_client_proxy.py b/tests/actor/test_client_proxy.py index 0dd051f9..0f828279 100644 --- a/tests/actor/test_client_proxy.py +++ b/tests/actor/test_client_proxy.py @@ -25,10 +25,10 @@ from dapr.actor.id import ActorId # noqa: E402 from dapr.actor.client.proxy import ActorProxy # noqa: E402 from dapr.serializers import DefaultJSONSerializer # noqa: E402 -from tests.actor.fake_actor_classes import ( +from tests.actor.fake_actor_classes import ( # noqa: E402 FakeMultiInterfacesActor, FakeActorCls2Interface, -) # noqa: E402 +) from tests.actor.fake_client import FakeDaprActorClient # noqa: E402 diff --git a/tests/clients/test_secure_http_service_invocation_client.py b/tests/clients/test_secure_http_service_invocation_client.py index 160f3521..842faae8 100644 --- a/tests/clients/test_secure_http_service_invocation_client.py +++ b/tests/clients/test_secure_http_service_invocation_client.py @@ -15,7 +15,6 @@ import ssl import typing from asyncio import TimeoutError -from unittest.mock import patch from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider @@ -117,13 +116,3 @@ def test_timeout_exception_thrown_when_timeout_reached(self): self.server.set_server_delay(1.5) with self.assertRaises(TimeoutError): new_client.invoke_method(self.app_id, self.method_name, '') - - @patch.object(settings, 'DAPR_HTTP_ENDPOINT', None) - def test_get_api_url_default(self): - client = DaprClient() - self.assertEqual( - 'http://{}:{}/{}'.format( - settings.DAPR_RUNTIME_HOST, settings.DAPR_HTTP_PORT, settings.DAPR_API_VERSION - ), - client.invocation_client._client.get_api_url(), - ) From d9a6e52c2749a4da43f333666cffc517c72fa76e Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Fri, 9 Feb 2024 18:48:41 +0000 Subject: [PATCH 13/29] Removes healthcheck from examples Signed-off-by: Elena Kolevska --- examples/configuration/configuration.py | 8 +- examples/error_handling/error_handling.py | 74 ++++----- examples/state_store/state_store.py | 149 ++++++++---------- .../state_store_query/state_store_query.py | 36 ++--- 4 files changed, 119 insertions(+), 148 deletions(-) diff --git a/examples/configuration/configuration.py b/examples/configuration/configuration.py index 9c13a088..ecf4baad 100644 --- a/examples/configuration/configuration.py +++ b/examples/configuration/configuration.py @@ -21,7 +21,6 @@ def handler(id: str, resp: ConfigurationResponse): ) -@healthcheck(5) async def executeConfiguration(): with DaprClient() as d: storeName = 'configurationstore' @@ -53,9 +52,4 @@ async def executeConfiguration(): print(f'Unsubscribed successfully? {isSuccess}', flush=True) -try: - asyncio.run(executeConfiguration()) -except TimeoutError as e: - print("Dapr wasn't ready in time: {e}") -except Exception as e: - print(e) +asyncio.run(executeConfiguration()) diff --git a/examples/error_handling/error_handling.py b/examples/error_handling/error_handling.py index 3e2f3199..11ba0da1 100644 --- a/examples/error_handling/error_handling.py +++ b/examples/error_handling/error_handling.py @@ -1,47 +1,41 @@ from dapr.clients import DaprClient from dapr.clients.exceptions import DaprGrpcError -from dapr.clients.health import healthcheck -@healthcheck(5) -def run_example(): - with DaprClient() as d: - storeName = 'statestore' +with DaprClient() as d: + storeName = 'statestore' - key = 'key||' - value = 'value_1' + key = 'key||' + value = 'value_1' - # Save single state. - try: - d.save_state(store_name=storeName, key=key, value=value) - except DaprGrpcError as err: - print(f'Status code: {err.code()}', flush=True) - print(f'Message: {err.details()}', flush=True) - print(f'Error code: {err.error_code()}', flush=True) + # Save single state. + try: + d.save_state(store_name=storeName, key=key, value=value) + except DaprGrpcError as err: + print(f'Status code: {err.code()}', flush=True) + print(f'Message: {err.details()}', flush=True) + print(f'Error code: {err.error_code()}', flush=True) - if err.status_details().error_info is not None: - print( - f'Error info(reason): {err.status_details().error_info["reason"]}', flush=True - ) - if err.status_details().resource_info is not None: - print( - f'Resource info (resource type): {err.status_details().resource_info["resource_type"]}', - flush=True, - ) - print( - f'Resource info (resource name): {err.status_details().resource_info["resource_name"]}', - flush=True, - ) - if err.status_details().bad_request is not None: - print( - f'Bad request (field): {err.status_details().bad_request["field_violations"][0]["field"]}', - flush=True, - ) - print( - f'Bad request (description): {err.status_details().bad_request["field_violations"][0]["description"]}', - flush=True, - ) - print(f'JSON: {err.json()}', flush=True) - - -run_example() + if err.status_details().error_info is not None: + print( + f'Error info(reason): {err.status_details().error_info["reason"]}', flush=True + ) + if err.status_details().resource_info is not None: + print( + f'Resource info (resource type): {err.status_details().resource_info["resource_type"]}', + flush=True, + ) + print( + f'Resource info (resource name): {err.status_details().resource_info["resource_name"]}', + flush=True, + ) + if err.status_details().bad_request is not None: + print( + f'Bad request (field): {err.status_details().bad_request["field_violations"][0]["field"]}', + flush=True, + ) + print( + f'Bad request (description): {err.status_details().bad_request["field_violations"][0]["description"]}', + flush=True, + ) + print(f'JSON: {err.json()}', flush=True) \ No newline at end of file diff --git a/examples/state_store/state_store.py b/examples/state_store/state_store.py index 56ec942f..7debb2f7 100644 --- a/examples/state_store/state_store.py +++ b/examples/state_store/state_store.py @@ -8,96 +8,87 @@ from dapr.clients.grpc._request import TransactionalStateOperation, TransactionOperationType from dapr.clients.grpc._state import StateItem -from dapr.clients.health import healthcheck -@healthcheck(5) -def save_state(): - with DaprClient() as d: - storeName = 'statestore' +with DaprClient() as d: + storeName = 'statestore' - key = 'key_1' - value = 'value_1' - updated_value = 'value_1_updated' + key = 'key_1' + value = 'value_1' + updated_value = 'value_1_updated' - another_key = 'key_2' - another_value = 'value_2' + another_key = 'key_2' + another_value = 'value_2' - yet_another_key = 'key_3' - yet_another_value = 'value_3' + yet_another_key = 'key_3' + yet_another_value = 'value_3' - # Save single state. - d.save_state(store_name=storeName, key=key, value=value) - print(f'State store has successfully saved {value} with {key} as key') + # Save single state. + d.save_state(store_name=storeName, key=key, value=value) + print(f'State store has successfully saved {value} with {key} as key') - # Save with an etag that is different from the one stored in the database. - try: - d.save_state(store_name=storeName, key=key, value=another_value, etag='9999') - except grpc.RpcError as err: - # StatusCode should be StatusCode.ABORTED. - print(f'Cannot save due to bad etag. ErrorCode={err.code()}') + # Save with an etag that is different from the one stored in the database. + try: + d.save_state(store_name=storeName, key=key, value=another_value, etag='9999') + except grpc.RpcError as err: + # StatusCode should be StatusCode.ABORTED. + print(f'Cannot save due to bad etag. ErrorCode={err.code()}') - # For detailed error messages from the dapr runtime: # print(f"Details={err.details()}) + # For detailed error messages from the dapr runtime: # print(f"Details={err.details()}) - # Save multiple states. + # Save multiple states. + d.save_bulk_state( + store_name=storeName, + states=[ + StateItem(key=another_key, value=another_value), + StateItem(key=yet_another_key, value=yet_another_value), + ], + ) + print(f'State store has successfully saved {another_value} with {another_key} as key') + print( + f'State store has successfully saved {yet_another_value} with {yet_another_key} as key' + ) + + # Save bulk with etag that is different from the one stored in the database. + try: d.save_bulk_state( store_name=storeName, states=[ - StateItem(key=another_key, value=another_value), - StateItem(key=yet_another_key, value=yet_another_value), - ], - ) - print(f'State store has successfully saved {another_value} with {another_key} as key') - print( - f'State store has successfully saved {yet_another_value} with {yet_another_key} as key' - ) - - # Save bulk with etag that is different from the one stored in the database. - try: - d.save_bulk_state( - store_name=storeName, - states=[ - StateItem(key=another_key, value=another_value, etag='999'), - StateItem(key=yet_another_key, value=yet_another_value, etag='999'), - ], - ) - except grpc.RpcError as err: - # StatusCode should be StatusCode.ABORTED. - print(f'Cannot save bulk due to bad etags. ErrorCode={err.code()}') - - # For detailed error messages from the dapr runtime: # print(f"Details={err.details()}) - - # Get one state by key. - state = d.get_state(store_name=storeName, key=key, state_metadata={'metakey': 'metavalue'}) - print(f'Got value={state.data} eTag={state.etag}') - - # Transaction upsert - d.execute_state_transaction( - store_name=storeName, - operations=[ - TransactionalStateOperation( - operation_type=TransactionOperationType.upsert, - key=key, - data=updated_value, - etag=state.etag, - ), - TransactionalStateOperation(key=another_key, data=another_value), + StateItem(key=another_key, value=another_value, etag='999'), + StateItem(key=yet_another_key, value=yet_another_value, etag='999'), ], ) - - # Batch get - items = d.get_bulk_state( - store_name=storeName, keys=[key, another_key], states_metadata={'metakey': 'metavalue'} - ).items - print(f'Got items with etags: {[(i.data, i.etag) for i in items]}') - - # Delete one state by key. - d.delete_state(store_name=storeName, key=key, state_metadata={'metakey': 'metavalue'}) - data = d.get_state(store_name=storeName, key=key).data - print(f'Got value after delete: {data}') - - -try: - save_state() -except TimeoutError as e: - print(f'Error: {e}') + except grpc.RpcError as err: + # StatusCode should be StatusCode.ABORTED. + print(f'Cannot save bulk due to bad etags. ErrorCode={err.code()}') + + # For detailed error messages from the dapr runtime: # print(f"Details={err.details()}) + + # Get one state by key. + state = d.get_state(store_name=storeName, key=key, state_metadata={'metakey': 'metavalue'}) + print(f'Got value={state.data} eTag={state.etag}') + + # Transaction upsert + d.execute_state_transaction( + store_name=storeName, + operations=[ + TransactionalStateOperation( + operation_type=TransactionOperationType.upsert, + key=key, + data=updated_value, + etag=state.etag, + ), + TransactionalStateOperation(key=another_key, data=another_value), + ], + ) + + # Batch get + items = d.get_bulk_state( + store_name=storeName, keys=[key, another_key], states_metadata={'metakey': 'metavalue'} + ).items + print(f'Got items with etags: {[(i.data, i.etag) for i in items]}') + + # Delete one state by key. + d.delete_state(store_name=storeName, key=key, state_metadata={'metakey': 'metavalue'}) + data = d.get_state(store_name=storeName, key=key).data + print(f'Got value after delete: {data}') \ No newline at end of file diff --git a/examples/state_store_query/state_store_query.py b/examples/state_store_query/state_store_query.py index 994e2d57..f532f0eb 100644 --- a/examples/state_store_query/state_store_query.py +++ b/examples/state_store_query/state_store_query.py @@ -3,33 +3,25 @@ """ from dapr.clients import DaprClient -from dapr.clients.grpc._state import StateItem import json -from dapr.clients.health import healthcheck +with DaprClient() as d: + store_name = 'statestore' -@healthcheck(5) -def state_store_query(): - with DaprClient() as d: - store_name = 'statestore' + # Query the state store - # Query the state store + query = open('query.json', 'r').read() + res = d.query_state(store_name=store_name, query=query) + for r in res.results: + print(r.key, json.dumps(json.loads(str(r.value, 'UTF-8')), sort_keys=True)) + print('Token:', res.token) - query = open('query.json', 'r').read() - res = d.query_state(store_name=store_name, query=query) - for r in res.results: - print(r.key, json.dumps(json.loads(str(r.value, 'UTF-8')), sort_keys=True)) - print('Token:', res.token) + # Get more results using a pagination token - # Get more results using a pagination token - - query = open('query-token.json', 'r').read() - res = d.query_state(store_name=store_name, query=query) - for r in res.results: - print(r.key, json.dumps(json.loads(str(r.value, 'UTF-8')), sort_keys=True)) - print('Token:', res.token) - - -state_store_query() + query = open('query-token.json', 'r').read() + res = d.query_state(store_name=store_name, query=query) + for r in res.results: + print(r.key, json.dumps(json.loads(str(r.value, 'UTF-8')), sort_keys=True)) + print('Token:', res.token) From b21df04f178354760e15f497aa6f4ccb0325d269 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Fri, 9 Feb 2024 18:52:43 +0000 Subject: [PATCH 14/29] Ruff Signed-off-by: Elena Kolevska --- examples/error_handling/error_handling.py | 6 ++---- examples/state_store/state_store.py | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/examples/error_handling/error_handling.py b/examples/error_handling/error_handling.py index 11ba0da1..b75ebed9 100644 --- a/examples/error_handling/error_handling.py +++ b/examples/error_handling/error_handling.py @@ -17,9 +17,7 @@ print(f'Error code: {err.error_code()}', flush=True) if err.status_details().error_info is not None: - print( - f'Error info(reason): {err.status_details().error_info["reason"]}', flush=True - ) + print(f'Error info(reason): {err.status_details().error_info["reason"]}', flush=True) if err.status_details().resource_info is not None: print( f'Resource info (resource type): {err.status_details().resource_info["resource_type"]}', @@ -38,4 +36,4 @@ f'Bad request (description): {err.status_details().bad_request["field_violations"][0]["description"]}', flush=True, ) - print(f'JSON: {err.json()}', flush=True) \ No newline at end of file + print(f'JSON: {err.json()}', flush=True) diff --git a/examples/state_store/state_store.py b/examples/state_store/state_store.py index 7debb2f7..1213226e 100644 --- a/examples/state_store/state_store.py +++ b/examples/state_store/state_store.py @@ -45,9 +45,7 @@ ], ) print(f'State store has successfully saved {another_value} with {another_key} as key') - print( - f'State store has successfully saved {yet_another_value} with {yet_another_key} as key' - ) + print(f'State store has successfully saved {yet_another_value} with {yet_another_key} as key') # Save bulk with etag that is different from the one stored in the database. try: @@ -91,4 +89,4 @@ # Delete one state by key. d.delete_state(store_name=storeName, key=key, state_metadata={'metakey': 'metavalue'}) data = d.get_state(store_name=storeName, key=key).data - print(f'Got value after delete: {data}') \ No newline at end of file + print(f'Got value after delete: {data}') From 495f78bccb9ae4a878bae67b50611753273b2c4d Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Sat, 10 Feb 2024 21:13:36 +0000 Subject: [PATCH 15/29] wip Signed-off-by: Elena Kolevska --- dapr/aio/clients/grpc/client.py | 3 + dapr/clients/grpc/client.py | 5 +- dapr/clients/health.py | 124 ++++++++++-------- dapr/clients/http/client.py | 5 +- dapr/conf/global_settings.py | 1 + examples/configuration/configuration.py | 1 - ext/dapr-ext-fastapi/tests/test_app.py | 6 +- ext/dapr-ext-fastapi/tests/test_dapractor.py | 10 -- ext/flask_dapr/tests/test_app.py | 7 - tests/actor/test_actor.py | 28 ++-- tests/actor/test_actor_reentrancy.py | 5 - tests/actor/test_actor_runtime.py | 5 - tests/actor/test_client_proxy.py | 21 +-- tests/clients/fake_dapr_server.py | 27 ++-- tests/clients/fake_http_server.py | 7 +- tests/clients/test_dapr_async_grpc_client.py | 88 +++++++------ tests/clients/test_dapr_grpc_client.py | 90 ++++++------- tests/clients/test_exceptions.py | 16 ++- tests/clients/test_health_decorator.py | 16 +-- .../test_http_service_invocation_client.py | 7 +- .../test_secure_dapr_async_grpc_client.py | 9 +- tests/clients/test_secure_dapr_grpc_client.py | 7 +- ...t_secure_http_service_invocation_client.py | 2 +- 23 files changed, 239 insertions(+), 251 deletions(-) diff --git a/dapr/aio/clients/grpc/client.py b/dapr/aio/clients/grpc/client.py index 498bed9a..c64df2f6 100644 --- a/dapr/aio/clients/grpc/client.py +++ b/dapr/aio/clients/grpc/client.py @@ -41,6 +41,7 @@ from dapr.clients.exceptions import DaprInternalError from dapr.clients.grpc._state import StateOptions, StateItem from dapr.clients.grpc._helpers import getWorkflowRuntimeStatus +from dapr.clients.health import CheckDaprHealth from dapr.conf.helpers import GrpcEndpoint from dapr.conf import settings from dapr.proto import api_v1, api_service_v1, common_v1 @@ -128,6 +129,8 @@ def __init__( max_grpc_messsage_length (int, optional): The maximum grpc send and receive message length in bytes. """ + CheckDaprHealth() + useragent = f'dapr-sdk-python/{__version__}' if not max_grpc_message_length: options = [ diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index f6a4927a..b551d328 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -40,7 +40,7 @@ from dapr.clients.exceptions import DaprInternalError, DaprGrpcError from dapr.clients.grpc._state import StateOptions, StateItem from dapr.clients.grpc._helpers import getWorkflowRuntimeStatus -from dapr.clients.health import healthcheck +from dapr.clients.health import CheckDaprHealth from dapr.conf import settings from dapr.proto import api_v1, api_service_v1, common_v1 from dapr.proto.runtime.v1.dapr_pb2 import UnsubscribeConfigurationResponse @@ -102,7 +102,6 @@ class DaprGrpcClient: ... resp = d.invoke_method('callee', 'method', b'data') """ - @healthcheck(5) def __init__( self, address: Optional[str] = None, @@ -129,6 +128,8 @@ def __init__( max_grpc_messsage_length (int, optional): The maximum grpc send and receive message length in bytes. """ + CheckDaprHealth() + useragent = f'dapr-sdk-python/{__version__}' if not max_grpc_message_length: options = [ diff --git a/dapr/clients/health.py b/dapr/clients/health.py index 72d33eb7..7f1d8362 100644 --- a/dapr/clients/health.py +++ b/dapr/clients/health.py @@ -12,71 +12,85 @@ See the License for the specific language governing permissions and limitations under the License. """ -import asyncio -import inspect import urllib.request import urllib.error import time -from functools import wraps - -import aiohttp from dapr.clients.http.conf import DAPR_API_TOKEN_HEADER, USER_AGENT_HEADER, DAPR_USER_AGENT from dapr.clients.http.helpers import get_api_url from dapr.conf import settings -HEALTHY = False - - -def healthcheck(timeout_s: int = 5): - def decorator(func): - health_url = f'{get_api_url()}/healthz/outbound' - headers = {USER_AGENT_HEADER: DAPR_USER_AGENT} - if settings.DAPR_API_TOKEN is not None: - headers[DAPR_API_TOKEN_HEADER] = settings.DAPR_API_TOKEN - start = time.time() - - @wraps(func) - async def async_wrapper(*args, **kwargs): - # Async health check logic - async with aiohttp.ClientSession() as session: - global HEALTHY - while not HEALTHY: - try: - async with session.get(health_url, headers=headers) as response: - if 200 <= response.status < 300: - HEALTHY = True - break - except aiohttp.ClientError as e: - print(f'Health check failed: {e}') - remaining = (start + timeout_s) - time.time() - if remaining <= 0: - raise TimeoutError(f'Dapr health check timed out, after {timeout_s}.') - await asyncio.sleep(min(1, remaining)) - return await func(*args, **kwargs) +# def healthcheck(): +# def decorator(func): +# timeout = settings.DAPR_HEALTH_TIMEOUT +# health_url = f'{get_api_url()}/healthz/outbound' +# headers = {USER_AGENT_HEADER: DAPR_USER_AGENT} +# if settings.DAPR_API_TOKEN is not None: +# headers[DAPR_API_TOKEN_HEADER] = settings.DAPR_API_TOKEN +# start = time.time() +# +# @wraps(func) +# async def async_wrapper(*args, **kwargs): +# # Async health check logic +# async with aiohttp.ClientSession() as session: +# while True: +# try: +# async with session.get(health_url, headers=headers) as response: +# if 200 <= response.status < 300: +# break +# except aiohttp.ClientError as e: +# print(f'Health check failed: {e}') +# +# remaining = (start + timeout) - time.time() +# if remaining <= 0: +# raise TimeoutError(f'Dapr health check timed out, after {timeout}.') +# await asyncio.sleep(min(1, remaining)) +# return await func(*args, **kwargs) +# +# def sync_wrapper(*args, **kwargs): +# while True: +# try: +# req = urllib.request.Request(health_url, headers=headers) +# with urllib.request.urlopen(req) as response: +# if 200 <= response.status < 300: +# break +# except urllib.error.URLError as e: +# print(f'Health check on {health_url} failed: {e.reason}') +# +# remaining = (start + timeout) - time.time() +# if remaining <= 0: +# raise TimeoutError(f'Dapr health check timed out, after {timeout}.') +# time.sleep(min(1, remaining)) +# return func(*args, **kwargs) +# +# if inspect.iscoroutinefunction(func): +# return async_wrapper +# else: +# return sync_wrapper +# +# return decorator - def sync_wrapper(*args, **kwargs): - global HEALTHY - while not HEALTHY: - try: - req = urllib.request.Request(health_url, headers=headers) - with urllib.request.urlopen(req) as response: - if 200 <= response.status < 300: - HEALTHY = True - break - except urllib.error.URLError as e: - print(f'Health check on {health_url} failed: {e.reason}') - remaining = (start + timeout_s) - time.time() - if remaining <= 0: - raise TimeoutError(f'Dapr health check timed out, after {timeout_s}.') - time.sleep(min(1, remaining)) - return func(*args, **kwargs) +class CheckDaprHealth: + def __init__(self): + self.health_url = f'{get_api_url()}/healthz/outbound' + self.headers = {USER_AGENT_HEADER: DAPR_USER_AGENT} + if settings.DAPR_API_TOKEN is not None: + self.headers[DAPR_API_TOKEN_HEADER] = settings.DAPR_API_TOKEN + self.timeout = settings.DAPR_HEALTH_TIMEOUT - if inspect.iscoroutinefunction(func): - return async_wrapper - else: - return sync_wrapper + start = time.time() + while True: + try: + req = urllib.request.Request(self.health_url, headers=self.headers) + with urllib.request.urlopen(req) as response: + if 200 <= response.status < 300: + break + except urllib.error.URLError as e: + print(f'Health check on {self.health_url} failed: {e.reason}') - return decorator + remaining = (start + self.timeout) - time.time() + if remaining <= 0: + raise TimeoutError(f'Dapr health check timed out, after {self.timeout}.') + time.sleep(min(1, remaining)) diff --git a/dapr/clients/http/client.py b/dapr/clients/http/client.py index a00b8ed0..98302d8c 100644 --- a/dapr/clients/http/client.py +++ b/dapr/clients/http/client.py @@ -23,7 +23,7 @@ DAPR_USER_AGENT, CONTENT_TYPE_HEADER, ) -from dapr.clients.health import healthcheck +from dapr.clients.health import CheckDaprHealth if TYPE_CHECKING: from dapr.serializers import Serializer @@ -36,7 +36,6 @@ class DaprHttpClient: """A Dapr Http API client""" - @healthcheck(5) def __init__( self, message_serializer: 'Serializer', @@ -50,6 +49,8 @@ def __init__( timeout (int, optional): Timeout in seconds, defaults to 60. headers_callback (lambda: Dict[str, str]], optional): Generates header for each request. """ + CheckDaprHealth() + self._timeout = aiohttp.ClientTimeout(total=timeout) self._serializer = message_serializer self._headers_callback = headers_callback diff --git a/dapr/conf/global_settings.py b/dapr/conf/global_settings.py index 5fe5647f..4bb201e4 100644 --- a/dapr/conf/global_settings.py +++ b/dapr/conf/global_settings.py @@ -25,6 +25,7 @@ DAPR_HTTP_PORT = 3500 DAPR_GRPC_PORT = 50001 DAPR_API_VERSION = 'v1.0' +DAPR_HEALTH_TIMEOUT = 5 # seconds DAPR_API_METHOD_INVOCATION_PROTOCOL = 'http' diff --git a/examples/configuration/configuration.py b/examples/configuration/configuration.py index ecf4baad..caf676e6 100644 --- a/examples/configuration/configuration.py +++ b/examples/configuration/configuration.py @@ -6,7 +6,6 @@ from time import sleep from dapr.clients import DaprClient from dapr.clients.grpc._response import ConfigurationWatcher, ConfigurationResponse -from dapr.clients.health import healthcheck configuration: ConfigurationWatcher = ConfigurationWatcher() diff --git a/ext/dapr-ext-fastapi/tests/test_app.py b/ext/dapr-ext-fastapi/tests/test_app.py index 0ed98d12..fa61fac7 100644 --- a/ext/dapr-ext-fastapi/tests/test_app.py +++ b/ext/dapr-ext-fastapi/tests/test_app.py @@ -4,11 +4,7 @@ from pydantic import BaseModel from fastapi.testclient import TestClient -from dapr.clients import health - -health.HEALTHY = True - -from dapr.ext.fastapi import DaprApp # noqa: E402 +from dapr.ext.fastapi import DaprApp class Message(BaseModel): diff --git a/ext/dapr-ext-fastapi/tests/test_dapractor.py b/ext/dapr-ext-fastapi/tests/test_dapractor.py index dc363182..ee863d72 100644 --- a/ext/dapr-ext-fastapi/tests/test_dapractor.py +++ b/ext/dapr-ext-fastapi/tests/test_dapractor.py @@ -20,18 +20,8 @@ from dapr.ext.fastapi.actor import DaprActor, _wrap_response -from dapr.clients import health - -health.HEALTHY = True - class DaprActorTest(unittest.TestCase): - def setUp(self): - health.HEALTHY = True - - def tearDown(self): - health.HEALTHY = False - def test_wrap_response_str(self): r = _wrap_response(200, 'fake_message') self.assertEqual({'message': 'fake_message'}, json.loads(r.body)) diff --git a/ext/flask_dapr/tests/test_app.py b/ext/flask_dapr/tests/test_app.py index 7ffa9a22..2665bb8a 100644 --- a/ext/flask_dapr/tests/test_app.py +++ b/ext/flask_dapr/tests/test_app.py @@ -2,10 +2,6 @@ from flask import Flask import json -from dapr.clients import health - -health.HEALTHY = True - from flask_dapr import DaprApp # noqa: E402 @@ -16,9 +12,6 @@ def setUp(self): self.dapr_app = DaprApp(self.app) self.client = self.app.test_client() - def tearDown(self): - health.HEALTHY = False - def test_subscribe_subscription_registered(self): @self.dapr_app.subscribe(pubsub='pubsub', topic='test') def event_handler(): diff --git a/tests/actor/test_actor.py b/tests/actor/test_actor.py index 93eea9a9..2838a8aa 100644 --- a/tests/actor/test_actor.py +++ b/tests/actor/test_actor.py @@ -18,40 +18,32 @@ from unittest import mock from datetime import timedelta -from dapr.clients import health - -health.HEALTHY = True - -from dapr.actor.id import ActorId # noqa: E402 -from dapr.actor.runtime.config import ActorRuntimeConfig # noqa: E402 -from dapr.actor.runtime.context import ActorRuntimeContext # noqa: E402 -from dapr.actor.runtime.runtime import ActorRuntime # noqa: E402 -from dapr.actor.runtime._type_information import ActorTypeInformation # noqa: E402 -from dapr.serializers import DefaultJSONSerializer # noqa: E402 - -from tests.actor.fake_actor_classes import ( # noqa: E402 +from dapr.actor.id import ActorId +from dapr.actor.runtime.config import ActorRuntimeConfig +from dapr.actor.runtime.context import ActorRuntimeContext +from dapr.actor.runtime.runtime import ActorRuntime +from dapr.actor.runtime._type_information import ActorTypeInformation +from dapr.serializers import DefaultJSONSerializer + +from tests.actor.fake_actor_classes import ( FakeSimpleActor, FakeSimpleReminderActor, FakeSimpleTimerActor, FakeMultiInterfacesActor, ) -from tests.actor.fake_client import FakeDaprActorClient # noqa: E402 -from tests.actor.utils import _async_mock, _run # noqa: E402 +from tests.actor.fake_client import FakeDaprActorClient +from tests.actor.utils import _async_mock, _run class ActorTests(unittest.TestCase): def setUp(self): - health.HEALTHY = True ActorRuntime._actor_managers = {} ActorRuntime.set_actor_config(ActorRuntimeConfig()) self._serializer = DefaultJSONSerializer() _run(ActorRuntime.register_actor(FakeSimpleActor)) _run(ActorRuntime.register_actor(FakeMultiInterfacesActor)) - def tearDown(self): - health.HEALTHY = False - def test_get_registered_actor_types(self): actor_types = ActorRuntime.get_registered_actor_types() self.assertTrue(actor_types.index('FakeSimpleActor') >= 0) diff --git a/tests/actor/test_actor_reentrancy.py b/tests/actor/test_actor_reentrancy.py index ad46948f..40b948a5 100644 --- a/tests/actor/test_actor_reentrancy.py +++ b/tests/actor/test_actor_reentrancy.py @@ -20,7 +20,6 @@ from dapr.actor.runtime.runtime import ActorRuntime from dapr.actor.runtime.config import ActorRuntimeConfig, ActorReentrancyConfig -from dapr.clients import health from dapr.serializers import DefaultJSONSerializer from tests.actor.fake_actor_classes import ( @@ -34,7 +33,6 @@ class ActorRuntimeTests(unittest.TestCase): def setUp(self): - health.HEALTHY = True ActorRuntime._actor_managers = {} ActorRuntime.set_actor_config( ActorRuntimeConfig(reentrancy=ActorReentrancyConfig(enabled=True)) @@ -44,9 +42,6 @@ def setUp(self): _run(ActorRuntime.register_actor(FakeSlowReentrantActor)) _run(ActorRuntime.register_actor(FakeMultiInterfacesActor)) - def tearDown(self): - health.HEALTHY = False - def test_reentrant_dispatch(self): _run(ActorRuntime.register_actor(FakeMultiInterfacesActor)) diff --git a/tests/actor/test_actor_runtime.py b/tests/actor/test_actor_runtime.py index a480da7d..3aa24289 100644 --- a/tests/actor/test_actor_runtime.py +++ b/tests/actor/test_actor_runtime.py @@ -19,7 +19,6 @@ from dapr.actor.runtime.runtime import ActorRuntime from dapr.actor.runtime.config import ActorRuntimeConfig -from dapr.clients import health from dapr.serializers import DefaultJSONSerializer from tests.actor.fake_actor_classes import ( @@ -33,7 +32,6 @@ class ActorRuntimeTests(unittest.TestCase): def setUp(self): - health.HEALTHY = True ActorRuntime._actor_managers = {} ActorRuntime.set_actor_config(ActorRuntimeConfig()) self._serializer = DefaultJSONSerializer() @@ -41,9 +39,6 @@ def setUp(self): _run(ActorRuntime.register_actor(FakeMultiInterfacesActor)) _run(ActorRuntime.register_actor(FakeSimpleTimerActor)) - def tearDown(self): - health.HEALTHY = False - def test_get_registered_actor_types(self): actor_types = ActorRuntime.get_registered_actor_types() self.assertTrue(actor_types.index('FakeSimpleActor') >= 0) diff --git a/tests/actor/test_client_proxy.py b/tests/actor/test_client_proxy.py index 0f828279..df448fb4 100644 --- a/tests/actor/test_client_proxy.py +++ b/tests/actor/test_client_proxy.py @@ -16,24 +16,19 @@ from unittest import mock -from dapr.clients import health -# Set the HEALTHY variable right after importing the health module -health.HEALTHY = True - - -from dapr.actor.id import ActorId # noqa: E402 -from dapr.actor.client.proxy import ActorProxy # noqa: E402 -from dapr.serializers import DefaultJSONSerializer # noqa: E402 -from tests.actor.fake_actor_classes import ( # noqa: E402 +from dapr.actor.id import ActorId +from dapr.actor.client.proxy import ActorProxy +from dapr.serializers import DefaultJSONSerializer +from tests.actor.fake_actor_classes import ( FakeMultiInterfacesActor, FakeActorCls2Interface, ) -from tests.actor.fake_client import FakeDaprActorClient # noqa: E402 +from tests.actor.fake_client import FakeDaprActorClient -from tests.actor.utils import _async_mock, _run # noqa: E402 +from tests.actor.utils import _async_mock, _run class FakeActoryProxyFactory: @@ -50,7 +45,6 @@ def create(self, actor_interface, actor_type, actor_id) -> ActorProxy: class ActorProxyTests(unittest.TestCase): def setUp(self): print('\nStarting test ..') - health.HEALTHY = True # Create mock client self._fake_client = FakeDaprActorClient self._fake_factory = FakeActoryProxyFactory(self._fake_client) @@ -61,9 +55,6 @@ def setUp(self): self._fake_factory, ) - def tearDown(self): - health.HEALTHY = False - @mock.patch( 'tests.actor.fake_client.FakeDaprActorClient.invoke_method', new=_async_mock(return_value=b'"expected_response"'), diff --git a/tests/clients/fake_dapr_server.py b/tests/clients/fake_dapr_server.py index b63a9cf2..45ab4a36 100644 --- a/tests/clients/fake_dapr_server.py +++ b/tests/clients/fake_dapr_server.py @@ -6,7 +6,6 @@ from google.protobuf import empty_pb2 from grpc_status import rpc_status -from dapr.clients import health from dapr.clients.grpc._helpers import to_bytes from dapr.proto import api_service_v1, common_v1, api_v1 from dapr.proto.common.v1.common_pb2 import ConfigurationItem @@ -39,11 +38,15 @@ PRIVATE_KEY_PATH, CERTIFICATE_CHAIN_PATH, ) +from tests.clients.fake_http_server import FakeHttpServer class FakeDaprSidecar(api_service_v1.DaprServicer): - def __init__(self): + def __init__(self, grpc_port: int = 50001, http_port: int = 8080): + self.grpc_port = grpc_port + self.http_port = http_port self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + self._http_server = FakeHttpServer(self.http_port) api_service_v1.add_DaprServicer_to_server(self, self._server) self.store = {} self.shutdown_received = False @@ -53,13 +56,14 @@ def __init__(self): self.metadata: Dict[str, str] = {} self._next_exception = None - def start(self, port: int = 8080): - health.HEALTHY = True - self._server.add_insecure_port(f'[::]:{port}') + def start( + self, + ): + self._server.add_insecure_port(f'[::]:{self.grpc_port}') self._server.start() + self._http_server.start() - def start_secure(self, port: int = 4443): - health.HEALTHY = True + def start_secure(self): create_certificates() private_key_file = open(PRIVATE_KEY_PATH, 'rb') @@ -75,15 +79,18 @@ def start_secure(self, port: int = 4443): [(private_key_content, certificate_chain_content)] ) - self._server.add_secure_port(f'[::]:{port}', credentials) + self._server.add_secure_port(f'[::]:{self.grpc_port}', credentials) self._server.start() + # The http server is only needed for the healthcheck endpoint + # so it can be started without a certificate + self._http_server.start() def stop(self): - health.HEALTHY = False + self._http_server.shutdown_server() self._server.stop(None) def stop_secure(self): - health.HEALTHY = False + self._http_server.shutdown_server() self._server.stop(None) delete_certificates() diff --git a/tests/clients/fake_http_server.py b/tests/clients/fake_http_server.py index c5bd15d1..2aa5f39a 100644 --- a/tests/clients/fake_http_server.py +++ b/tests/clients/fake_http_server.py @@ -4,7 +4,6 @@ from threading import Thread from http.server import BaseHTTPRequestHandler, HTTPServer -from dapr.clients import health from tests.clients.certs import ( CERTIFICATE_CHAIN_PATH, PRIVATE_KEY_PATH, @@ -54,11 +53,11 @@ def do_DELETE(self): class FakeHttpServer(Thread): - def __init__(self, secure=False): + def __init__(self, port: int = 8080, secure=False): super().__init__() self.secure = secure - self.port = 4443 if secure else 8080 + self.port = port self.server = HTTPServer(('localhost', self.port), DaprHandler) if self.secure: @@ -83,7 +82,6 @@ def get_request_headers(self): return self.server.request_headers def shutdown_server(self): - health.HEALTHY = False self.server.shutdown() self.server.socket.close() self.join() @@ -104,5 +102,4 @@ def set_server_delay(self, delay_seconds): self.server.sleep_time = delay_seconds def run(self): - health.HEALTHY = True self.server.serve_forever() diff --git a/tests/clients/test_dapr_async_grpc_client.py b/tests/clients/test_dapr_async_grpc_client.py index f0539f76..0d59f69b 100644 --- a/tests/clients/test_dapr_async_grpc_client.py +++ b/tests/clients/test_dapr_async_grpc_client.py @@ -37,18 +37,20 @@ class DaprGrpcClientAsyncTests(unittest.IsolatedAsyncioTestCase): - server_port = 8080 + grpc_port = 50001 + http_port = 3500 scheme = '' def setUp(self): - self._fake_dapr_server = FakeDaprSidecar() - self._fake_dapr_server.start(self.server_port) + self._fake_dapr_server = FakeDaprSidecar(grpc_port=self.grpc_port, http_port=self.http_port) + settings.DAPR_HTTP_PORT = self.http_port + self._fake_dapr_server.start() def tearDown(self): self._fake_dapr_server.stop() async def test_http_extension(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') # Test POST verb without querystring ext = dapr._get_http_extension('POST') @@ -70,7 +72,7 @@ async def test_http_extension(self): self.assertEqual('query1=string1&query2=string2&query1=string+3', ext.querystring) async def test_invoke_method_bytes_data(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') resp = await dapr.invoke_method( app_id='targetId', method_name='bytes', @@ -89,7 +91,7 @@ async def test_invoke_method_bytes_data(self): self.assertEqual(['value1'], resp.headers['hkey1']) async def test_invoke_method_no_data(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') resp = await dapr.invoke_method( app_id='targetId', method_name='bytes', @@ -107,7 +109,7 @@ async def test_invoke_method_no_data(self): self.assertEqual(['value1'], resp.headers['hkey1']) async def test_invoke_method_with_dapr_client(self): - dapr = DaprClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprClient(f'{self.scheme}localhost:{self.grpc_port}') dapr.invocation_client = None # force to use grpc client resp = await dapr.invoke_method( @@ -127,7 +129,7 @@ async def test_invoke_method_with_dapr_client(self): self.assertEqual(['value1'], resp.headers['hkey1']) async def test_invoke_method_proto_data(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') req = common_v1.StateItem(key='test') resp = await dapr.invoke_method( app_id='targetId', @@ -149,7 +151,7 @@ async def test_invoke_method_proto_data(self): self.assertEqual('test', new_resp.key) async def test_invoke_binding_bytes_data(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') resp = await dapr.invoke_binding( binding_name='binding', operation='create', @@ -166,7 +168,7 @@ async def test_invoke_binding_bytes_data(self): self.assertEqual(['value1'], resp.headers['hkey1']) async def test_invoke_binding_no_metadata(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') resp = await dapr.invoke_binding( binding_name='binding', operation='create', @@ -178,7 +180,7 @@ async def test_invoke_binding_no_metadata(self): self.assertEqual(0, len(resp.headers)) async def test_invoke_binding_no_data(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') resp = await dapr.invoke_binding( binding_name='binding', operation='create', @@ -189,7 +191,7 @@ async def test_invoke_binding_no_data(self): self.assertEqual(0, len(resp.headers)) async def test_invoke_binding_no_create(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') resp = await dapr.invoke_binding( binding_name='binding', operation='delete', @@ -201,14 +203,14 @@ async def test_invoke_binding_no_create(self): self.assertEqual(0, len(resp.headers)) async def test_publish_event(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') resp = await dapr.publish_event(pubsub_name='pubsub', topic_name='example', data=b'haha') self.assertEqual(2, len(resp.headers)) self.assertEqual(['haha'], resp.headers['hdata']) async def test_publish_event_with_content_type(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') resp = await dapr.publish_event( pubsub_name='pubsub', topic_name='example', @@ -221,7 +223,7 @@ async def test_publish_event_with_content_type(self): self.assertEqual(['application/json'], resp.headers['data_content_type']) async def test_publish_event_with_metadata(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') resp = await dapr.publish_event( pubsub_name='pubsub', topic_name='example', @@ -235,7 +237,7 @@ async def test_publish_event_with_metadata(self): self.assertEqual(['100'], resp.headers['metadata_ttl_in_seconds']) async def test_publish_error(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') with self.assertRaisesRegex(ValueError, "invalid type for data "): await dapr.publish_event( pubsub_name='pubsub', @@ -245,7 +247,7 @@ async def test_publish_error(self): @patch.object(settings, 'DAPR_API_TOKEN', 'test-token') async def test_dapr_api_token_insertion(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') resp = await dapr.invoke_method( app_id='targetId', method_name='bytes', @@ -264,7 +266,7 @@ async def test_dapr_api_token_insertion(self): self.assertEqual(['test-token'], resp.headers['hdapr-api-token']) async def test_get_save_delete_state(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') key = 'key_1' value = 'value_1' options = StateOptions( @@ -305,7 +307,7 @@ async def test_get_save_delete_state(self): self.assertTrue('delete failed' in str(context.exception)) async def test_get_save_state_etag_none(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') value = 'test' no_etag_key = 'no_etag' @@ -327,7 +329,7 @@ async def test_get_save_state_etag_none(self): self.assertEqual(resp.etag, '') async def test_transaction_then_get_states(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') key = str(uuid.uuid4()) value = str(uuid.uuid4()) @@ -360,7 +362,7 @@ async def test_transaction_then_get_states(self): self.assertEqual(resp.items[1].data, to_bytes(another_value.upper())) async def test_save_then_get_states(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') key = str(uuid.uuid4()) value = str(uuid.uuid4()) @@ -395,7 +397,7 @@ async def test_save_then_get_states(self): self.assertEqual(resp.items[1].data, to_bytes(another_value.upper())) async def test_get_secret(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') key1 = 'key_1' resp = await dapr.get_secret( store_name='store_1', @@ -411,7 +413,7 @@ async def test_get_secret(self): self.assertEqual({key1: 'val'}, resp._secret) async def test_get_secret_metadata_absent(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') key1 = 'key_1' resp = await dapr.get_secret( store_name='store_1', @@ -423,7 +425,7 @@ async def test_get_secret_metadata_absent(self): self.assertEqual({key1: 'val'}, resp._secret) async def test_get_bulk_secret(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') resp = await dapr.get_bulk_secret( store_name='store_1', metadata=( @@ -437,7 +439,7 @@ async def test_get_bulk_secret(self): self.assertEqual({'keya': {'keyb': 'val'}}, resp._secrets) async def test_get_bulk_secret_metadata_absent(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') resp = await dapr.get_bulk_secret(store_name='store_1') self.assertEqual(1, len(resp.headers)) @@ -445,7 +447,7 @@ async def test_get_bulk_secret_metadata_absent(self): self.assertEqual({'keya': {'keyb': 'val'}}, resp._secrets) async def test_get_configuration(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') keys = ['k', 'k1'] value = 'value' version = '1.5.0' @@ -470,7 +472,7 @@ async def test_get_configuration(self): self.assertEqual(item.metadata, metadata) async def test_subscribe_configuration(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') def mock_watch(self, stub, store_name, keys, handler, config_metadata): handler( @@ -491,12 +493,12 @@ def handler(id: str, resp: ConfigurationResponse): ) async def test_unsubscribe_configuration(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') res = await dapr.unsubscribe_configuration(store_name='configurationstore', id='k') self.assertTrue(res) async def test_query_state(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') resp = await dapr.query_state( store_name='statestore', @@ -513,12 +515,12 @@ async def test_query_state(self): self.assertEqual(len(resp.results), 3) async def test_shutdown(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') await dapr.shutdown() self.assertTrue(self._fake_dapr_server.shutdown_received) async def test_wait_ok(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') await dapr.wait(0.1) async def test_wait_timeout(self): @@ -533,7 +535,7 @@ async def test_wait_timeout(self): self.assertTrue('Connection refused' in str(context.exception)) async def test_lock_acquire_success(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') # Lock parameters store_name = 'lockstore' resource_id = str(uuid.uuid4()) @@ -546,7 +548,7 @@ async def test_lock_acquire_success(self): self.assertEqual(UnlockResponseStatus.success, unlock_response.status) async def test_lock_release_twice_fails(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') # Lock parameters store_name = 'lockstore' resource_id = str(uuid.uuid4()) @@ -562,7 +564,7 @@ async def test_lock_release_twice_fails(self): self.assertEqual(UnlockResponseStatus.lock_does_not_exist, unlock_response.status) async def test_lock_conflict(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') # Lock parameters store_name = 'lockstore' resource_id = str(uuid.uuid4()) @@ -584,14 +586,14 @@ async def test_lock_conflict(self): self.assertEqual(UnlockResponseStatus.success, unlock_response.status) async def test_lock_not_previously_acquired(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') unlock_response = await dapr.unlock( store_name='lockstore', resource_id=str(uuid.uuid4()), lock_owner=str(uuid.uuid4()) ) self.assertEqual(UnlockResponseStatus.lock_does_not_exist, unlock_response.status) async def test_lock_release_twice_fails_with_context_manager(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') # Lock parameters store_name = 'lockstore' resource_id = str(uuid.uuid4()) @@ -614,7 +616,7 @@ async def test_lock_release_twice_fails_with_context_manager(self): self.assertEqual(UnlockResponseStatus.lock_does_not_exist, unlock_response.status) async def test_lock_are_not_reentrant(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') # Lock parameters store_name = 'lockstore' resource_id = str(uuid.uuid4()) @@ -632,7 +634,7 @@ async def test_lock_are_not_reentrant(self): self.assertFalse(second_attempt.success) async def test_lock_input_validation(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') # Sane parameters store_name = 'lockstore' resource_id = str(uuid.uuid4()) @@ -667,7 +669,7 @@ async def test_lock_input_validation(self): self.assertTrue(res.success) async def test_unlock_input_validation(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') # Sane parameters store_name = 'lockstore' resource_id = str(uuid.uuid4()) @@ -689,7 +691,7 @@ async def test_unlock_input_validation(self): # async def test_get_metadata(self): - async with DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') as dapr: + async with DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') as dapr: response = await dapr.get_metadata() self.assertIsNotNone(response) @@ -719,7 +721,7 @@ async def test_get_metadata(self): async def test_set_metadata(self): metadata_key = 'test_set_metadata_attempt' - async with DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') as dapr: + async with DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') as dapr: for metadata_value in [str(i) for i in range(10)]: await dapr.set_metadata(attributeName=metadata_key, attributeValue=metadata_value) response = await dapr.get_metadata() @@ -736,11 +738,11 @@ async def test_set_metadata(self): self.assertEqual(response.extended_metadata[metadata_key], metadata_value) async def test_set_metadata_input_validation(self): - dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') valid_attr_name = 'attribute name' valid_attr_value = 'attribute value' # Invalid inputs for string arguments - async with DaprGrpcClientAsync(f'{self.scheme}localhost:{self.server_port}') as dapr: + async with DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') as dapr: for invalid_attr_name in [None, '', ' ']: with self.assertRaises(ValueError): await dapr.set_metadata(invalid_attr_name, valid_attr_value) diff --git a/tests/clients/test_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client.py index 6480bcb5..c350a68e 100644 --- a/tests/clients/test_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client.py @@ -42,19 +42,21 @@ class DaprGrpcClientTests(unittest.TestCase): - server_port = 8080 + grpc_port = 50001 + http_port = 3500 scheme = '' error = None def setUp(self): - self._fake_dapr_server = FakeDaprSidecar() - self._fake_dapr_server.start(self.server_port) + self._fake_dapr_server = FakeDaprSidecar(grpc_port=self.grpc_port, http_port=self.http_port) + settings.DAPR_HTTP_PORT = self.http_port + self._fake_dapr_server.start() def tearDown(self): self._fake_dapr_server.stop() def test_http_extension(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') # Test POST verb without querystring ext = dapr._get_http_extension('POST') @@ -76,7 +78,7 @@ def test_http_extension(self): self.assertEqual('query1=string1&query2=string2&query1=string+3', ext.querystring) def test_invoke_method_bytes_data(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') resp = dapr.invoke_method( app_id='targetId', method_name='bytes', @@ -95,7 +97,7 @@ def test_invoke_method_bytes_data(self): self.assertEqual(['value1'], resp.headers['hkey1']) def test_invoke_method_no_data(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') resp = dapr.invoke_method( app_id='targetId', method_name='bytes', @@ -113,7 +115,7 @@ def test_invoke_method_no_data(self): self.assertEqual(['value1'], resp.headers['hkey1']) def test_invoke_method_async(self): - dapr = DaprClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprClient(f'{self.scheme}localhost:{self.grpc_port}') dapr.invocation_client = None # force to use grpc client with self.assertRaises(NotImplementedError): @@ -133,7 +135,7 @@ def test_invoke_method_async(self): ) def test_invoke_method_proto_data(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') req = common_v1.StateItem(key='test') resp = dapr.invoke_method( app_id='targetId', @@ -155,7 +157,7 @@ def test_invoke_method_proto_data(self): self.assertEqual('test', new_resp.key) def test_invoke_binding_bytes_data(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') resp = dapr.invoke_binding( binding_name='binding', operation='create', @@ -172,7 +174,7 @@ def test_invoke_binding_bytes_data(self): self.assertEqual(['value1'], resp.headers['hkey1']) def test_invoke_binding_no_metadata(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') resp = dapr.invoke_binding( binding_name='binding', operation='create', @@ -184,7 +186,7 @@ def test_invoke_binding_no_metadata(self): self.assertEqual(0, len(resp.headers)) def test_invoke_binding_no_data(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') resp = dapr.invoke_binding( binding_name='binding', operation='create', @@ -195,7 +197,7 @@ def test_invoke_binding_no_data(self): self.assertEqual(0, len(resp.headers)) def test_invoke_binding_no_create(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') resp = dapr.invoke_binding( binding_name='binding', operation='delete', @@ -207,7 +209,7 @@ def test_invoke_binding_no_create(self): self.assertEqual(0, len(resp.headers)) def test_publish_event(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') resp = dapr.publish_event(pubsub_name='pubsub', topic_name='example', data=b'test_data') self.assertEqual(2, len(resp.headers)) @@ -220,7 +222,7 @@ def test_publish_event(self): dapr.publish_event(pubsub_name='pubsub', topic_name='example', data=b'test_data') def test_publish_event_with_content_type(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') resp = dapr.publish_event( pubsub_name='pubsub', topic_name='example', @@ -233,7 +235,7 @@ def test_publish_event_with_content_type(self): self.assertEqual(['application/json'], resp.headers['data_content_type']) def test_publish_event_with_metadata(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') resp = dapr.publish_event( pubsub_name='pubsub', topic_name='example', @@ -247,7 +249,7 @@ def test_publish_event_with_metadata(self): self.assertEqual(['100'], resp.headers['metadata_ttl_in_seconds']) def test_publish_error(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') with self.assertRaisesRegex(ValueError, "invalid type for data "): dapr.publish_event( pubsub_name='pubsub', @@ -257,7 +259,7 @@ def test_publish_error(self): @patch.object(settings, 'DAPR_API_TOKEN', 'test-token') def test_dapr_api_token_insertion(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') resp = dapr.invoke_method( app_id='targetId', method_name='bytes', @@ -276,7 +278,7 @@ def test_dapr_api_token_insertion(self): self.assertEqual(['test-token'], resp.headers['hdapr-api-token']) def test_get_save_delete_state(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') key = 'key_1' value = 'value_1' options = StateOptions( @@ -322,7 +324,7 @@ def test_get_save_delete_state(self): self.assertTrue('delete failed' in str(context.exception)) def test_get_save_state_etag_none(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') value = 'test' no_etag_key = 'no_etag' @@ -344,7 +346,7 @@ def test_get_save_state_etag_none(self): self.assertEqual(resp.etag, '') def test_transaction_then_get_states(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') key = str(uuid.uuid4()) value = str(uuid.uuid4()) @@ -390,7 +392,7 @@ def test_transaction_then_get_states(self): ) def test_bulk_save_then_get_states(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') key = str(uuid.uuid4()) value = str(uuid.uuid4()) @@ -446,7 +448,7 @@ def test_bulk_save_then_get_states(self): ) def test_get_secret(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') key1 = 'key_1' resp = dapr.get_secret( store_name='store_1', @@ -462,7 +464,7 @@ def test_get_secret(self): self.assertEqual({key1: 'val'}, resp._secret) def test_get_secret_metadata_absent(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') key1 = 'key_1' resp = dapr.get_secret( store_name='store_1', @@ -474,7 +476,7 @@ def test_get_secret_metadata_absent(self): self.assertEqual({key1: 'val'}, resp._secret) def test_get_bulk_secret(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') resp = dapr.get_bulk_secret( store_name='store_1', metadata=( @@ -488,7 +490,7 @@ def test_get_bulk_secret(self): self.assertEqual({'keya': {'keyb': 'val'}}, resp._secrets) def test_get_bulk_secret_metadata_absent(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') resp = dapr.get_bulk_secret(store_name='store_1') self.assertEqual(1, len(resp.headers)) @@ -496,7 +498,7 @@ def test_get_bulk_secret_metadata_absent(self): self.assertEqual({'keya': {'keyb': 'val'}}, resp._secrets) def test_get_configuration(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') keys = ['k', 'k1'] value = 'value' version = '1.5.0' @@ -521,7 +523,7 @@ def test_get_configuration(self): self.assertEqual(item.metadata, metadata) def test_subscribe_configuration(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') def mock_watch(self, stub, store_name, keys, handler, config_metadata): handler( @@ -542,12 +544,12 @@ def handler(id: str, resp: ConfigurationResponse): ) def test_unsubscribe_configuration(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') res = dapr.unsubscribe_configuration(store_name='configurationstore', id='k') self.assertTrue(res) def test_query_state(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') resp = dapr.query_state( store_name='statestore', @@ -573,12 +575,12 @@ def test_query_state(self): ) def test_shutdown(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') dapr.shutdown() self.assertTrue(self._fake_dapr_server.shutdown_received) def test_wait_ok(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') dapr.wait(0.1) def test_wait_timeout(self): @@ -593,7 +595,7 @@ def test_wait_timeout(self): self.assertTrue('Connection refused' in str(context.exception)) def test_lock_acquire_success(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') # Lock parameters store_name = 'lockstore' resource_id = str(uuid.uuid4()) @@ -606,7 +608,7 @@ def test_lock_acquire_success(self): self.assertEqual(UnlockResponseStatus.success, unlock_response.status) def test_lock_release_twice_fails(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') # Lock parameters store_name = 'lockstore' resource_id = str(uuid.uuid4()) @@ -622,7 +624,7 @@ def test_lock_release_twice_fails(self): self.assertEqual(UnlockResponseStatus.lock_does_not_exist, unlock_response.status) def test_lock_conflict(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') # Lock parameters store_name = 'lockstore' resource_id = str(uuid.uuid4()) @@ -644,14 +646,14 @@ def test_lock_conflict(self): self.assertEqual(UnlockResponseStatus.success, unlock_response.status) def test_lock_not_previously_acquired(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') unlock_response = dapr.unlock( store_name='lockstore', resource_id=str(uuid.uuid4()), lock_owner=str(uuid.uuid4()) ) self.assertEqual(UnlockResponseStatus.lock_does_not_exist, unlock_response.status) def test_lock_release_twice_fails_with_context_manager(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') # Lock parameters store_name = 'lockstore' resource_id = str(uuid.uuid4()) @@ -670,7 +672,7 @@ def test_lock_release_twice_fails_with_context_manager(self): self.assertEqual(UnlockResponseStatus.lock_does_not_exist, unlock_response.status) def test_lock_are_not_reentrant(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') # Lock parameters store_name = 'lockstore' resource_id = str(uuid.uuid4()) @@ -684,7 +686,7 @@ def test_lock_are_not_reentrant(self): self.assertFalse(second_attempt.success) def test_lock_input_validation(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') # Sane parameters store_name = 'lockstore' resource_id = str(uuid.uuid4()) @@ -711,7 +713,7 @@ def test_lock_input_validation(self): self.assertTrue(res.success) def test_unlock_input_validation(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') # Sane parameters store_name = 'lockstore' resource_id = str(uuid.uuid4()) @@ -733,7 +735,7 @@ def test_unlock_input_validation(self): # def test_workflow(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') # Sane parameters workflow_name = 'test_workflow' event_name = 'eventName' @@ -798,7 +800,7 @@ def test_workflow(self): # def test_get_metadata(self): - with DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') as dapr: + with DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') as dapr: response = dapr.get_metadata() self.assertIsNotNone(response) @@ -828,7 +830,7 @@ def test_get_metadata(self): def test_set_metadata(self): metadata_key = 'test_set_metadata_attempt' - with DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') as dapr: + with DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') as dapr: for metadata_value in [str(i) for i in range(10)]: dapr.set_metadata(attributeName=metadata_key, attributeValue=metadata_value) response = dapr.get_metadata() @@ -845,11 +847,11 @@ def test_set_metadata(self): self.assertEqual(response.extended_metadata[metadata_key], metadata_value) def test_set_metadata_input_validation(self): - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') valid_attr_name = 'attribute name' valid_attr_value = 'attribute value' # Invalid inputs for string arguments - with DaprGrpcClient(f'{self.scheme}localhost:{self.server_port}') as dapr: + with DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') as dapr: for invalid_attr_name in [None, '', ' ']: with self.assertRaises(ValueError): dapr.set_metadata(invalid_attr_name, valid_attr_value) diff --git a/tests/clients/test_exceptions.py b/tests/clients/test_exceptions.py index d6f2890a..093b0ddf 100644 --- a/tests/clients/test_exceptions.py +++ b/tests/clients/test_exceptions.py @@ -7,6 +7,7 @@ from dapr.clients import DaprGrpcClient from dapr.clients.exceptions import DaprGrpcError +from dapr.conf import settings from .fake_dapr_server import FakeDaprSidecar @@ -89,17 +90,22 @@ def create_expected_status(): class DaprExceptionsTestCase(unittest.TestCase): + _grpc_port = 50001 + _http_port = 3500 + def setUp(self): - self._server_port = 8080 - self._fake_dapr_server = FakeDaprSidecar() - self._fake_dapr_server.start(self._server_port) + self._fake_dapr_server = FakeDaprSidecar( + grpc_port=self._grpc_port, http_port=self._http_port + ) + settings.DAPR_HTTP_PORT = self._http_port + self._fake_dapr_server.start() self._expected_status = create_expected_status() def tearDown(self): self._fake_dapr_server.stop() def test_exception_status_parsing(self): - dapr = DaprGrpcClient(f'localhost:{self._server_port}') + dapr = DaprGrpcClient(f'localhost:{self._grpc_port}') self._fake_dapr_server.raise_exception_on_next_call(self._expected_status) with self.assertRaises(DaprGrpcError) as context: @@ -186,7 +192,7 @@ def test_exception_status_parsing(self): ) def test_error_code(self): - dapr = DaprGrpcClient(f'localhost:{self._server_port}') + dapr = DaprGrpcClient(f'localhost:{self._grpc_port}') expected_status = create_expected_status() diff --git a/tests/clients/test_health_decorator.py b/tests/clients/test_health_decorator.py index a2f4649d..702253db 100644 --- a/tests/clients/test_health_decorator.py +++ b/tests/clients/test_health_decorator.py @@ -6,10 +6,6 @@ class TestHealthCheckDecorator(unittest.TestCase): - def tearDown(self): - # Reset the global var to true, because it's needed for other tests - health.HEALTHY = True - @patch('urllib.request.urlopen') def test_healthcheck_sync(self, mock_urlopen): # Mock the response to simulate a healthy Dapr service @@ -17,7 +13,7 @@ def test_healthcheck_sync(self, mock_urlopen): health.HEALTHY = False - @health.healthcheck(1) + @health.healthcheck() def sync_test_function(): return 'Sync function executed' @@ -33,7 +29,7 @@ def test_healthcheck_sync_unhealthy(self, mock_urlopen): health.HEALTHY = False - @health.healthcheck(1) + @health.healthcheck() def sync_test_function(): return 'Sync function executed' @@ -49,7 +45,7 @@ def test_healthcheck_sync_unhealthy_with_global_healthy(self, mock_urlopen): health.HEALTHY = True - @health.healthcheck(1) + @health.healthcheck() def sync_test_function(): return 'Sync function executed' @@ -68,7 +64,7 @@ def test_healthcheck_async(self, mock_get): health.HEALTHY = False - @health.healthcheck(1) + @health.healthcheck() async def async_test_function(): return 'Async function executed' @@ -87,7 +83,7 @@ def test_healthcheck_async_unhealthy(self, mock_get): health.HEALTHY = False - @health.healthcheck(1) + @health.healthcheck() async def async_test_function(): return 'Async function executed' @@ -106,7 +102,7 @@ def test_healthcheck_async_unhealthy_with_global(self, mock_get): health.HEALTHY = True - @health.healthcheck(1) + @health.healthcheck() async def async_test_function(): return 'Async function executed' diff --git a/tests/clients/test_http_service_invocation_client.py b/tests/clients/test_http_service_invocation_client.py index f40c5d0d..e1a0304b 100644 --- a/tests/clients/test_http_service_invocation_client.py +++ b/tests/clients/test_http_service_invocation_client.py @@ -24,22 +24,25 @@ from opentelemetry.sdk.trace.sampling import ALWAYS_ON from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator -from dapr.clients import DaprClient + from dapr.clients.exceptions import DaprInternalError from dapr.conf import settings from dapr.proto import common_v1 from .fake_http_server import FakeHttpServer +from dapr.clients import DaprClient class DaprInvocationHttpClientTests(unittest.TestCase): def setUp(self): - self.server = FakeHttpServer() + self.server = FakeHttpServer(port=8080) self.server_port = self.server.get_port() self.server.start() settings.DAPR_HTTP_PORT = self.server_port settings.DAPR_API_METHOD_INVOCATION_PROTOCOL = 'http' + self.client = DaprClient() + self.app_id = 'fakeapp' self.method_name = 'fakemethod' self.invoke_url = f'/v1.0/invoke/{self.app_id}/method/{self.method_name}' diff --git a/tests/clients/test_secure_dapr_async_grpc_client.py b/tests/clients/test_secure_dapr_async_grpc_client.py index 37a4e7c0..b505a49c 100644 --- a/tests/clients/test_secure_dapr_async_grpc_client.py +++ b/tests/clients/test_secure_dapr_async_grpc_client.py @@ -40,12 +40,15 @@ def replacement_get_credentials_func(a): class DaprSecureGrpcClientAsyncTests(DaprGrpcClientAsyncTests): - server_port = 4443 + grpc_port = 50001 + http_port = 4443 scheme = 'https://' def setUp(self): - self._fake_dapr_server = FakeDaprSidecar() - self._fake_dapr_server.start_secure(self.server_port) + self._fake_dapr_server = FakeDaprSidecar(grpc_port=self.grpc_port, http_port=self.http_port) + settings.DAPR_HTTP_PORT = self.http_port + settings.DAPR_HTTP_ENDPOINT = 'http://127.0.0.1:{}'.format(self.http_port) + self._fake_dapr_server.start_secure() def tearDown(self): self._fake_dapr_server.stop_secure() diff --git a/tests/clients/test_secure_dapr_grpc_client.py b/tests/clients/test_secure_dapr_grpc_client.py index 07a128ea..db9290ea 100644 --- a/tests/clients/test_secure_dapr_grpc_client.py +++ b/tests/clients/test_secure_dapr_grpc_client.py @@ -38,12 +38,13 @@ def replacement_get_credentials_func(a): class DaprSecureGrpcClientTests(DaprGrpcClientTests): - server_port = 4443 + grpc_port = 50001 + http_port = 4443 scheme = 'https://' def setUp(self): - self._fake_dapr_server = FakeDaprSidecar() - self._fake_dapr_server.start_secure(self.server_port) + self._fake_dapr_server = FakeDaprSidecar(grpc_port=self.grpc_port, http_port=self.http_port) + self._fake_dapr_server.start_secure() def tearDown(self): self._fake_dapr_server.stop_secure() diff --git a/tests/clients/test_secure_http_service_invocation_client.py b/tests/clients/test_secure_http_service_invocation_client.py index 842faae8..8e438763 100644 --- a/tests/clients/test_secure_http_service_invocation_client.py +++ b/tests/clients/test_secure_http_service_invocation_client.py @@ -44,7 +44,7 @@ class DaprSecureInvocationHttpClientTests(DaprInvocationHttpClientTests): def setUp(self): DaprHttpClient.get_ssl_context = replacement_get_ssl_context - self.server = FakeHttpServer(secure=True) + self.server = FakeHttpServer(secure=True, port=4443) self.server_port = self.server.get_port() self.server.start() settings.DAPR_HTTP_PORT = self.server_port From afdda30f9d3767017220a684d0aada2163561c42 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Sun, 11 Feb 2024 11:23:55 +0000 Subject: [PATCH 16/29] wip Signed-off-by: Elena Kolevska --- tests/actor/test_client_proxy.py | 1 - tests/clients/test_dapr_async_grpc_client.py | 1 + tests/clients/test_dapr_grpc_client.py | 1 + tests/clients/test_exceptions.py | 1 + tests/clients/test_secure_dapr_grpc_client.py | 2 ++ 5 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/actor/test_client_proxy.py b/tests/actor/test_client_proxy.py index df448fb4..fe667d62 100644 --- a/tests/actor/test_client_proxy.py +++ b/tests/actor/test_client_proxy.py @@ -44,7 +44,6 @@ def create(self, actor_interface, actor_type, actor_id) -> ActorProxy: class ActorProxyTests(unittest.TestCase): def setUp(self): - print('\nStarting test ..') # Create mock client self._fake_client = FakeDaprActorClient self._fake_factory = FakeActoryProxyFactory(self._fake_client) diff --git a/tests/clients/test_dapr_async_grpc_client.py b/tests/clients/test_dapr_async_grpc_client.py index 0d59f69b..abf9c2eb 100644 --- a/tests/clients/test_dapr_async_grpc_client.py +++ b/tests/clients/test_dapr_async_grpc_client.py @@ -44,6 +44,7 @@ class DaprGrpcClientAsyncTests(unittest.IsolatedAsyncioTestCase): def setUp(self): self._fake_dapr_server = FakeDaprSidecar(grpc_port=self.grpc_port, http_port=self.http_port) settings.DAPR_HTTP_PORT = self.http_port + settings.DAPR_HTTP_ENDPOINT = 'http://127.0.0.1:{}'.format(self.http_port) self._fake_dapr_server.start() def tearDown(self): diff --git a/tests/clients/test_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client.py index c350a68e..9e3ca04d 100644 --- a/tests/clients/test_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client.py @@ -50,6 +50,7 @@ class DaprGrpcClientTests(unittest.TestCase): def setUp(self): self._fake_dapr_server = FakeDaprSidecar(grpc_port=self.grpc_port, http_port=self.http_port) settings.DAPR_HTTP_PORT = self.http_port + settings.DAPR_HTTP_ENDPOINT = 'http://127.0.0.1:{}'.format(self.http_port) self._fake_dapr_server.start() def tearDown(self): diff --git a/tests/clients/test_exceptions.py b/tests/clients/test_exceptions.py index 093b0ddf..313666af 100644 --- a/tests/clients/test_exceptions.py +++ b/tests/clients/test_exceptions.py @@ -98,6 +98,7 @@ def setUp(self): grpc_port=self._grpc_port, http_port=self._http_port ) settings.DAPR_HTTP_PORT = self._http_port + settings.DAPR_HTTP_ENDPOINT = 'http://127.0.0.1:{}'.format(self._http_port) self._fake_dapr_server.start() self._expected_status = create_expected_status() diff --git a/tests/clients/test_secure_dapr_grpc_client.py b/tests/clients/test_secure_dapr_grpc_client.py index db9290ea..8d842453 100644 --- a/tests/clients/test_secure_dapr_grpc_client.py +++ b/tests/clients/test_secure_dapr_grpc_client.py @@ -44,6 +44,8 @@ class DaprSecureGrpcClientTests(DaprGrpcClientTests): def setUp(self): self._fake_dapr_server = FakeDaprSidecar(grpc_port=self.grpc_port, http_port=self.http_port) + settings.DAPR_HTTP_PORT = self.http_port + settings.DAPR_HTTP_ENDPOINT = 'http://127.0.0.1:{}'.format(self.http_port) self._fake_dapr_server.start_secure() def tearDown(self): From 96247953c584cebcbc0c87b52f7ab241de24f641 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Sun, 11 Feb 2024 21:53:17 +0000 Subject: [PATCH 17/29] wip Signed-off-by: Elena Kolevska --- dapr/actor/client/proxy.py | 12 +- dapr/aio/clients/grpc/client.py | 4 +- dapr/clients/grpc/client.py | 4 +- dapr/clients/health.py | 80 +++---------- dapr/clients/http/client.py | 4 +- tests/actor/test_actor.py | 9 ++ tests/actor/test_actor_reentrancy.py | 9 ++ tests/actor/test_actor_runtime.py | 9 ++ tests/clients/fake_dapr_server.py | 4 +- tests/clients/test_health_decorator.py | 113 ------------------ .../test_http_service_invocation_client.py | 2 +- tests/clients/test_secure_dapr_grpc_client.py | 7 +- ...t_secure_http_service_invocation_client.py | 23 +++- 13 files changed, 88 insertions(+), 192 deletions(-) delete mode 100644 tests/clients/test_health_decorator.py diff --git a/dapr/actor/client/proxy.py b/dapr/actor/client/proxy.py index e7baa90c..77304bb6 100644 --- a/dapr/actor/client/proxy.py +++ b/dapr/actor/client/proxy.py @@ -98,7 +98,7 @@ class ActorProxy: communication. """ - _default_proxy_factory = ActorProxyFactory() + _default_proxy_factory = None def __init__( self, @@ -127,6 +127,13 @@ def actor_type(self) -> str: """Returns actor type.""" return self._actor_type + @classmethod + def _get_default_factory_instance(cls): + """Lazily initializes and returns the default ActorProxyFactory instance.""" + if cls._default_proxy_factory is None: + cls._default_proxy_factory = ActorProxyFactory() + return cls._default_proxy_factory + @classmethod def create( cls, @@ -146,8 +153,9 @@ def create( Returns: :class:`ActorProxy': new Actor Proxy client. + @param actor_proxy_factory: """ - factory = cls._default_proxy_factory if not actor_proxy_factory else actor_proxy_factory + factory = actor_proxy_factory if actor_proxy_factory else cls._get_default_factory_instance() return factory.create(actor_type, actor_id, actor_interface) async def invoke_method(self, method: str, raw_body: Optional[bytes] = None) -> bytes: diff --git a/dapr/aio/clients/grpc/client.py b/dapr/aio/clients/grpc/client.py index fba5de35..89f473e2 100644 --- a/dapr/aio/clients/grpc/client.py +++ b/dapr/aio/clients/grpc/client.py @@ -42,7 +42,7 @@ from dapr.clients.exceptions import DaprInternalError, DaprGrpcError from dapr.clients.grpc._state import StateOptions, StateItem from dapr.clients.grpc._helpers import getWorkflowRuntimeStatus -from dapr.clients.health import CheckDaprHealth +from dapr.clients.health import DaprHealth from dapr.conf.helpers import GrpcEndpoint from dapr.conf import settings from dapr.proto import api_v1, api_service_v1, common_v1 @@ -130,7 +130,7 @@ def __init__( max_grpc_messsage_length (int, optional): The maximum grpc send and receive message length in bytes. """ - CheckDaprHealth() + DaprHealth.wait_until_ready() useragent = f'dapr-sdk-python/{__version__}' if not max_grpc_message_length: diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index b551d328..55ee735d 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -40,7 +40,7 @@ from dapr.clients.exceptions import DaprInternalError, DaprGrpcError from dapr.clients.grpc._state import StateOptions, StateItem from dapr.clients.grpc._helpers import getWorkflowRuntimeStatus -from dapr.clients.health import CheckDaprHealth +from dapr.clients.health import DaprHealth from dapr.conf import settings from dapr.proto import api_v1, api_service_v1, common_v1 from dapr.proto.runtime.v1.dapr_pb2 import UnsubscribeConfigurationResponse @@ -128,7 +128,7 @@ def __init__( max_grpc_messsage_length (int, optional): The maximum grpc send and receive message length in bytes. """ - CheckDaprHealth() + DaprHealth.wait_until_ready() useragent = f'dapr-sdk-python/{__version__}' if not max_grpc_message_length: diff --git a/dapr/clients/health.py b/dapr/clients/health.py index 7f1d8362..a631efe6 100644 --- a/dapr/clients/health.py +++ b/dapr/clients/health.py @@ -21,76 +21,32 @@ from dapr.conf import settings -# def healthcheck(): -# def decorator(func): -# timeout = settings.DAPR_HEALTH_TIMEOUT -# health_url = f'{get_api_url()}/healthz/outbound' -# headers = {USER_AGENT_HEADER: DAPR_USER_AGENT} -# if settings.DAPR_API_TOKEN is not None: -# headers[DAPR_API_TOKEN_HEADER] = settings.DAPR_API_TOKEN -# start = time.time() -# -# @wraps(func) -# async def async_wrapper(*args, **kwargs): -# # Async health check logic -# async with aiohttp.ClientSession() as session: -# while True: -# try: -# async with session.get(health_url, headers=headers) as response: -# if 200 <= response.status < 300: -# break -# except aiohttp.ClientError as e: -# print(f'Health check failed: {e}') -# -# remaining = (start + timeout) - time.time() -# if remaining <= 0: -# raise TimeoutError(f'Dapr health check timed out, after {timeout}.') -# await asyncio.sleep(min(1, remaining)) -# return await func(*args, **kwargs) -# -# def sync_wrapper(*args, **kwargs): -# while True: -# try: -# req = urllib.request.Request(health_url, headers=headers) -# with urllib.request.urlopen(req) as response: -# if 200 <= response.status < 300: -# break -# except urllib.error.URLError as e: -# print(f'Health check on {health_url} failed: {e.reason}') -# -# remaining = (start + timeout) - time.time() -# if remaining <= 0: -# raise TimeoutError(f'Dapr health check timed out, after {timeout}.') -# time.sleep(min(1, remaining)) -# return func(*args, **kwargs) -# -# if inspect.iscoroutinefunction(func): -# return async_wrapper -# else: -# return sync_wrapper -# -# return decorator - - -class CheckDaprHealth: - def __init__(self): - self.health_url = f'{get_api_url()}/healthz/outbound' - self.headers = {USER_AGENT_HEADER: DAPR_USER_AGENT} +class DaprHealth: + @classmethod + def wait_until_ready(self): + health_url = f'{get_api_url()}/healthz/outbound' + headers = {USER_AGENT_HEADER: DAPR_USER_AGENT} if settings.DAPR_API_TOKEN is not None: - self.headers[DAPR_API_TOKEN_HEADER] = settings.DAPR_API_TOKEN - self.timeout = settings.DAPR_HEALTH_TIMEOUT + headers[DAPR_API_TOKEN_HEADER] = settings.DAPR_API_TOKEN + timeout = settings.DAPR_HEALTH_TIMEOUT start = time.time() while True: try: - req = urllib.request.Request(self.health_url, headers=self.headers) - with urllib.request.urlopen(req) as response: + req = urllib.request.Request(health_url, headers=headers) + with urllib.request.urlopen(req, context=self.get_context()) as response: if 200 <= response.status < 300: break except urllib.error.URLError as e: - print(f'Health check on {self.health_url} failed: {e.reason}') + print(f'Health check on {health_url} failed: {e.reason}') - remaining = (start + self.timeout) - time.time() + remaining = (start + timeout) - time.time() if remaining <= 0: - raise TimeoutError(f'Dapr health check timed out, after {self.timeout}.') + raise TimeoutError(f'Dapr health check timed out, after {timeout}.') time.sleep(min(1, remaining)) + + @classmethod + def get_context(cls): + # This method is used (overwritten) from tests + # to return context for self-signed certificates + return None diff --git a/dapr/clients/http/client.py b/dapr/clients/http/client.py index 98302d8c..0d591156 100644 --- a/dapr/clients/http/client.py +++ b/dapr/clients/http/client.py @@ -23,7 +23,7 @@ DAPR_USER_AGENT, CONTENT_TYPE_HEADER, ) -from dapr.clients.health import CheckDaprHealth +from dapr.clients.health import DaprHealth if TYPE_CHECKING: from dapr.serializers import Serializer @@ -49,7 +49,7 @@ def __init__( timeout (int, optional): Timeout in seconds, defaults to 60. headers_callback (lambda: Dict[str, str]], optional): Generates header for each request. """ - CheckDaprHealth() + DaprHealth.wait_until_ready() self._timeout = aiohttp.ClientTimeout(total=timeout) self._serializer = message_serializer diff --git a/tests/actor/test_actor.py b/tests/actor/test_actor.py index 2838a8aa..d2944f16 100644 --- a/tests/actor/test_actor.py +++ b/tests/actor/test_actor.py @@ -23,6 +23,7 @@ from dapr.actor.runtime.context import ActorRuntimeContext from dapr.actor.runtime.runtime import ActorRuntime from dapr.actor.runtime._type_information import ActorTypeInformation +from dapr.conf import settings from dapr.serializers import DefaultJSONSerializer from tests.actor.fake_actor_classes import ( @@ -34,16 +35,24 @@ from tests.actor.fake_client import FakeDaprActorClient from tests.actor.utils import _async_mock, _run +from tests.clients.fake_http_server import FakeHttpServer class ActorTests(unittest.TestCase): def setUp(self): + self.server = FakeHttpServer(port=3500) + self.server.start() + settings.DAPR_HTTP_PORT = 3500 + ActorRuntime._actor_managers = {} ActorRuntime.set_actor_config(ActorRuntimeConfig()) self._serializer = DefaultJSONSerializer() _run(ActorRuntime.register_actor(FakeSimpleActor)) _run(ActorRuntime.register_actor(FakeMultiInterfacesActor)) + def tearDown(self): + self.server.shutdown_server() + def test_get_registered_actor_types(self): actor_types = ActorRuntime.get_registered_actor_types() self.assertTrue(actor_types.index('FakeSimpleActor') >= 0) diff --git a/tests/actor/test_actor_reentrancy.py b/tests/actor/test_actor_reentrancy.py index 40b948a5..d2915900 100644 --- a/tests/actor/test_actor_reentrancy.py +++ b/tests/actor/test_actor_reentrancy.py @@ -20,6 +20,7 @@ from dapr.actor.runtime.runtime import ActorRuntime from dapr.actor.runtime.config import ActorRuntimeConfig, ActorReentrancyConfig +from dapr.conf import settings from dapr.serializers import DefaultJSONSerializer from tests.actor.fake_actor_classes import ( @@ -29,10 +30,15 @@ ) from tests.actor.utils import _run +from tests.clients.fake_http_server import FakeHttpServer class ActorRuntimeTests(unittest.TestCase): def setUp(self): + self.server = FakeHttpServer(port=3500) + self.server.start() + settings.DAPR_HTTP_PORT = 3500 + ActorRuntime._actor_managers = {} ActorRuntime.set_actor_config( ActorRuntimeConfig(reentrancy=ActorReentrancyConfig(enabled=True)) @@ -42,6 +48,9 @@ def setUp(self): _run(ActorRuntime.register_actor(FakeSlowReentrantActor)) _run(ActorRuntime.register_actor(FakeMultiInterfacesActor)) + def tearDown(self): + self.server.shutdown_server() + def test_reentrant_dispatch(self): _run(ActorRuntime.register_actor(FakeMultiInterfacesActor)) diff --git a/tests/actor/test_actor_runtime.py b/tests/actor/test_actor_runtime.py index 3aa24289..a38b30bc 100644 --- a/tests/actor/test_actor_runtime.py +++ b/tests/actor/test_actor_runtime.py @@ -19,6 +19,7 @@ from dapr.actor.runtime.runtime import ActorRuntime from dapr.actor.runtime.config import ActorRuntimeConfig +from dapr.conf import settings from dapr.serializers import DefaultJSONSerializer from tests.actor.fake_actor_classes import ( @@ -28,10 +29,15 @@ ) from tests.actor.utils import _run +from tests.clients.fake_http_server import FakeHttpServer class ActorRuntimeTests(unittest.TestCase): def setUp(self): + self.server = FakeHttpServer(port=3500) + self.server.start() + settings.DAPR_HTTP_PORT = 3500 + ActorRuntime._actor_managers = {} ActorRuntime.set_actor_config(ActorRuntimeConfig()) self._serializer = DefaultJSONSerializer() @@ -39,6 +45,9 @@ def setUp(self): _run(ActorRuntime.register_actor(FakeMultiInterfacesActor)) _run(ActorRuntime.register_actor(FakeSimpleTimerActor)) + def tearDown(self): + self.server.shutdown_server() + def test_get_registered_actor_types(self): actor_types = ActorRuntime.get_registered_actor_types() self.assertTrue(actor_types.index('FakeSimpleActor') >= 0) diff --git a/tests/clients/fake_dapr_server.py b/tests/clients/fake_dapr_server.py index 45ab4a36..778e2219 100644 --- a/tests/clients/fake_dapr_server.py +++ b/tests/clients/fake_dapr_server.py @@ -56,9 +56,7 @@ def __init__(self, grpc_port: int = 50001, http_port: int = 8080): self.metadata: Dict[str, str] = {} self._next_exception = None - def start( - self, - ): + def start(self, ): self._server.add_insecure_port(f'[::]:{self.grpc_port}') self._server.start() self._http_server.start() diff --git a/tests/clients/test_health_decorator.py b/tests/clients/test_health_decorator.py deleted file mode 100644 index 702253db..00000000 --- a/tests/clients/test_health_decorator.py +++ /dev/null @@ -1,113 +0,0 @@ -import asyncio -import unittest -from unittest.mock import patch, MagicMock - -from dapr.clients import health - - -class TestHealthCheckDecorator(unittest.TestCase): - @patch('urllib.request.urlopen') - def test_healthcheck_sync(self, mock_urlopen): - # Mock the response to simulate a healthy Dapr service - mock_urlopen.return_value.__enter__.return_value = MagicMock(status=200) - - health.HEALTHY = False - - @health.healthcheck() - def sync_test_function(): - return 'Sync function executed' - - result = sync_test_function() - self.assertEqual(result, 'Sync function executed') - mock_urlopen.assert_called() - - @patch('urllib.request.urlopen') - def test_healthcheck_sync_unhealthy(self, mock_urlopen): - # Mock the response to simulate an unhealthy Dapr service - - mock_urlopen.return_value.__enter__.return_value = MagicMock(status=500) - - health.HEALTHY = False - - @health.healthcheck() - def sync_test_function(): - return 'Sync function executed' - - with self.assertRaises(TimeoutError): - sync_test_function() - - mock_urlopen.assert_called() - - @patch('urllib.request.urlopen') - def test_healthcheck_sync_unhealthy_with_global_healthy(self, mock_urlopen): - # Mock the response to simulate an unhealthy Dapr service - mock_urlopen.return_value.__enter__.return_value = MagicMock(status=500) - - health.HEALTHY = True - - @health.healthcheck() - def sync_test_function(): - return 'Sync function executed' - - sync_test_function() - - # Assert we never called the health endpoint, because the global var has already been set - mock_urlopen.assert_not_called() - - -class TestHealthCheckDecoratorAsync(unittest.IsolatedAsyncioTestCase): - @patch('aiohttp.ClientSession.get') - def test_healthcheck_async(self, mock_get): - # Mock the response to simulate a healthy Dapr service - mock_response = MagicMock(status=200) - mock_get.return_value.__aenter__.return_value = mock_response - - health.HEALTHY = False - - @health.healthcheck() - async def async_test_function(): - return 'Async function executed' - - async def run_test(): - result = await async_test_function() - self.assertEqual(result, 'Async function executed') - - asyncio.run(run_test()) - mock_get.assert_called() - - @patch('aiohttp.ClientSession.get') - def test_healthcheck_async_unhealthy(self, mock_get): - # Mock the response to simulate an unhealthy Dapr service - mock_response = MagicMock(status=500) - mock_get.return_value.__aenter__.return_value = mock_response - - health.HEALTHY = False - - @health.healthcheck() - async def async_test_function(): - return 'Async function executed' - - async def run_test(): - with self.assertRaises(TimeoutError): - await async_test_function() - - asyncio.run(run_test()) - mock_get.assert_called() - - @patch('aiohttp.ClientSession.get') - def test_healthcheck_async_unhealthy_with_global(self, mock_get): - # Mock the response to simulate an unhealthy Dapr service - mock_response = MagicMock(status=500) - mock_get.return_value.__aenter__.return_value = mock_response - - health.HEALTHY = True - - @health.healthcheck() - async def async_test_function(): - return 'Async function executed' - - async def run_test(): - await async_test_function() - - asyncio.run(run_test()) - mock_get.assert_not_called() diff --git a/tests/clients/test_http_service_invocation_client.py b/tests/clients/test_http_service_invocation_client.py index e1a0304b..a5e16cfc 100644 --- a/tests/clients/test_http_service_invocation_client.py +++ b/tests/clients/test_http_service_invocation_client.py @@ -35,7 +35,7 @@ class DaprInvocationHttpClientTests(unittest.TestCase): def setUp(self): - self.server = FakeHttpServer(port=8080) + self.server = FakeHttpServer(port=3500) self.server_port = self.server.get_port() self.server.start() settings.DAPR_HTTP_PORT = self.server_port diff --git a/tests/clients/test_secure_dapr_grpc_client.py b/tests/clients/test_secure_dapr_grpc_client.py index 8d842453..f13bf455 100644 --- a/tests/clients/test_secure_dapr_grpc_client.py +++ b/tests/clients/test_secure_dapr_grpc_client.py @@ -23,10 +23,11 @@ from tests.clients.test_dapr_grpc_client import DaprGrpcClientTests from .fake_dapr_server import FakeDaprSidecar - -# Used temporarily, so we can trust self-signed certificates in unit tests -# until they get their own environment variable def replacement_get_credentials_func(a): + """ + Used temporarily, so we can trust self-signed certificates in unit tests + until they get their own environment variable + """ f = open(os.path.join(os.path.dirname(__file__), 'selfsigned.pem'), 'rb') creds = grpc.ssl_channel_credentials(f.read()) f.close() diff --git a/tests/clients/test_secure_http_service_invocation_client.py b/tests/clients/test_secure_http_service_invocation_client.py index 8e438763..1eeb6120 100644 --- a/tests/clients/test_secure_http_service_invocation_client.py +++ b/tests/clients/test_secure_http_service_invocation_client.py @@ -15,6 +15,7 @@ import ssl import typing from asyncio import TimeoutError +from unittest.mock import patch from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider @@ -23,6 +24,7 @@ from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator from dapr.clients import DaprClient +from dapr.clients.health import DaprHealth from dapr.clients.http.client import DaprHttpClient from dapr.conf import settings from dapr.proto import common_v1 @@ -33,16 +35,33 @@ from .test_http_service_invocation_client import DaprInvocationHttpClientTests -def replacement_get_ssl_context(a): +def replacement_get_client_ssl_context(a): + """ + This method is used (overwritten) from tests + to return context for self-signed certificates + """ ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ssl_context.load_verify_locations(CERTIFICATE_CHAIN_PATH) return ssl_context +def replacement_get_health_context(): + """ + This method is used (overwritten) from tests + to return context for self-signed certificates + """ + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + return context + + class DaprSecureInvocationHttpClientTests(DaprInvocationHttpClientTests): def setUp(self): - DaprHttpClient.get_ssl_context = replacement_get_ssl_context + DaprHttpClient.get_ssl_context = replacement_get_client_ssl_context + DaprHealth.get_context = replacement_get_health_context self.server = FakeHttpServer(secure=True, port=4443) self.server_port = self.server.get_port() From 476e8561a08db41f1666dd081c0ac5357643a48e Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Sun, 11 Feb 2024 22:01:20 +0000 Subject: [PATCH 18/29] wip Signed-off-by: Elena Kolevska --- dapr/actor/client/proxy.py | 4 +++- tests/clients/fake_dapr_server.py | 4 +++- tests/clients/test_dapr_async_grpc_client.py | 4 +++- tests/clients/test_secure_dapr_grpc_client.py | 1 + tests/clients/test_secure_http_service_invocation_client.py | 1 - 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/dapr/actor/client/proxy.py b/dapr/actor/client/proxy.py index 77304bb6..fd62d271 100644 --- a/dapr/actor/client/proxy.py +++ b/dapr/actor/client/proxy.py @@ -155,7 +155,9 @@ def create( :class:`ActorProxy': new Actor Proxy client. @param actor_proxy_factory: """ - factory = actor_proxy_factory if actor_proxy_factory else cls._get_default_factory_instance() + factory = ( + actor_proxy_factory if actor_proxy_factory else cls._get_default_factory_instance() + ) return factory.create(actor_type, actor_id, actor_interface) async def invoke_method(self, method: str, raw_body: Optional[bytes] = None) -> bytes: diff --git a/tests/clients/fake_dapr_server.py b/tests/clients/fake_dapr_server.py index 778e2219..45ab4a36 100644 --- a/tests/clients/fake_dapr_server.py +++ b/tests/clients/fake_dapr_server.py @@ -56,7 +56,9 @@ def __init__(self, grpc_port: int = 50001, http_port: int = 8080): self.metadata: Dict[str, str] = {} self._next_exception = None - def start(self, ): + def start( + self, + ): self._server.add_insecure_port(f'[::]:{self.grpc_port}') self._server.start() self._http_server.start() diff --git a/tests/clients/test_dapr_async_grpc_client.py b/tests/clients/test_dapr_async_grpc_client.py index b6971d39..0039f174 100644 --- a/tests/clients/test_dapr_async_grpc_client.py +++ b/tests/clients/test_dapr_async_grpc_client.py @@ -208,7 +208,9 @@ async def test_invoke_binding_no_create(self): async def test_publish_event(self): dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') - resp = await dapr.publish_event(pubsub_name='pubsub', topic_name='example', data=b'test_data') + resp = await dapr.publish_event( + pubsub_name='pubsub', topic_name='example', data=b'test_data' + ) self.assertEqual(2, len(resp.headers)) self.assertEqual(['test_data'], resp.headers['hdata']) diff --git a/tests/clients/test_secure_dapr_grpc_client.py b/tests/clients/test_secure_dapr_grpc_client.py index f13bf455..0d5cc2fe 100644 --- a/tests/clients/test_secure_dapr_grpc_client.py +++ b/tests/clients/test_secure_dapr_grpc_client.py @@ -23,6 +23,7 @@ from tests.clients.test_dapr_grpc_client import DaprGrpcClientTests from .fake_dapr_server import FakeDaprSidecar + def replacement_get_credentials_func(a): """ Used temporarily, so we can trust self-signed certificates in unit tests diff --git a/tests/clients/test_secure_http_service_invocation_client.py b/tests/clients/test_secure_http_service_invocation_client.py index 1eeb6120..528b078d 100644 --- a/tests/clients/test_secure_http_service_invocation_client.py +++ b/tests/clients/test_secure_http_service_invocation_client.py @@ -15,7 +15,6 @@ import ssl import typing from asyncio import TimeoutError -from unittest.mock import patch from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider From 70f948fddf6cf8dd90159bf91189c12dca21c157 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 12 Feb 2024 11:37:42 +0000 Subject: [PATCH 19/29] Set health timeout to 60 seconds Signed-off-by: Elena Kolevska --- dapr/conf/global_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dapr/conf/global_settings.py b/dapr/conf/global_settings.py index 4bb201e4..b7cb885b 100644 --- a/dapr/conf/global_settings.py +++ b/dapr/conf/global_settings.py @@ -25,7 +25,7 @@ DAPR_HTTP_PORT = 3500 DAPR_GRPC_PORT = 50001 DAPR_API_VERSION = 'v1.0' -DAPR_HEALTH_TIMEOUT = 5 # seconds +DAPR_HEALTH_TIMEOUT = 60 # seconds DAPR_API_METHOD_INVOCATION_PROTOCOL = 'http' From 47b9431bbafd87e6667b17b0dd19535c7b83d539 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 12 Feb 2024 16:28:11 +0000 Subject: [PATCH 20/29] wip Signed-off-by: Elena Kolevska --- dapr/clients/health.py | 4 +-- tests/clients/certs.py | 27 +++++++++++++++++++ .../test_secure_dapr_async_grpc_client.py | 18 +++---------- tests/clients/test_secure_dapr_grpc_client.py | 17 +++--------- 4 files changed, 36 insertions(+), 30 deletions(-) diff --git a/dapr/clients/health.py b/dapr/clients/health.py index a631efe6..835f6c6d 100644 --- a/dapr/clients/health.py +++ b/dapr/clients/health.py @@ -34,7 +34,7 @@ def wait_until_ready(self): while True: try: req = urllib.request.Request(health_url, headers=headers) - with urllib.request.urlopen(req, context=self.get_context()) as response: + with urllib.request.urlopen(req, context=self.get_ssl_context()) as response: if 200 <= response.status < 300: break except urllib.error.URLError as e: @@ -46,7 +46,7 @@ def wait_until_ready(self): time.sleep(min(1, remaining)) @classmethod - def get_context(cls): + def get_ssl_context(cls): # This method is used (overwritten) from tests # to return context for self-signed certificates return None diff --git a/tests/clients/certs.py b/tests/clients/certs.py index 5fb9b8a6..12f3b57e 100644 --- a/tests/clients/certs.py +++ b/tests/clients/certs.py @@ -1,7 +1,10 @@ import os +import ssl from OpenSSL import crypto +import grpc + PRIVATE_KEY_PATH = os.path.join(os.path.dirname(__file__), 'private.key') CERTIFICATE_CHAIN_PATH = os.path.join(os.path.dirname(__file__), 'selfsigned.pem') @@ -40,3 +43,27 @@ def delete_certificates(): if os.path.exists(CERTIFICATE_CHAIN_PATH): os.remove(CERTIFICATE_CHAIN_PATH) + + +def replacement_get_credentials_func(a): + """ + Used temporarily, so we can trust self-signed certificates in unit tests + until they get their own environment variable + """ + f = open(os.path.join(os.path.dirname(__file__), 'selfsigned.pem'), 'rb') + creds = grpc.ssl_channel_credentials(f.read()) + f.close() + + return creds + + +def replacement_get_health_context(): + """ + This method is used (overwritten) from tests + to return context for self-signed certificates + """ + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + return context diff --git a/tests/clients/test_secure_dapr_async_grpc_client.py b/tests/clients/test_secure_dapr_async_grpc_client.py index b505a49c..eb540eff 100644 --- a/tests/clients/test_secure_dapr_async_grpc_client.py +++ b/tests/clients/test_secure_dapr_async_grpc_client.py @@ -14,41 +14,31 @@ """ import os +import ssl import unittest from unittest.mock import patch -import grpc - from dapr.aio.clients.grpc.client import DaprGrpcClientAsync +from tests.clients.certs import replacement_get_credentials_func from tests.clients.test_dapr_async_grpc_client import DaprGrpcClientAsyncTests from .fake_dapr_server import FakeDaprSidecar from dapr.conf import settings -# Used temporarily, so we can trust self-signed certificates in unit tests -# until they get their own environment variable -def replacement_get_credentials_func(a): - f = open(os.path.join(os.path.dirname(__file__), 'selfsigned.pem'), 'rb') - creds = grpc.ssl_channel_credentials(f.read()) - f.close() - - return creds - - DaprGrpcClientAsync.get_credentials = replacement_get_credentials_func class DaprSecureGrpcClientAsyncTests(DaprGrpcClientAsyncTests): grpc_port = 50001 - http_port = 4443 + http_port = 3500 # The http server is used for health checks only, and doesn't need TLS scheme = 'https://' def setUp(self): self._fake_dapr_server = FakeDaprSidecar(grpc_port=self.grpc_port, http_port=self.http_port) + self._fake_dapr_server.start_secure() settings.DAPR_HTTP_PORT = self.http_port settings.DAPR_HTTP_ENDPOINT = 'http://127.0.0.1:{}'.format(self.http_port) - self._fake_dapr_server.start_secure() def tearDown(self): self._fake_dapr_server.stop_secure() diff --git a/tests/clients/test_secure_dapr_grpc_client.py b/tests/clients/test_secure_dapr_grpc_client.py index 0d5cc2fe..017bbd36 100644 --- a/tests/clients/test_secure_dapr_grpc_client.py +++ b/tests/clients/test_secure_dapr_grpc_client.py @@ -19,36 +19,25 @@ from dapr.clients.grpc.client import DaprGrpcClient from dapr.conf import settings +from tests.clients.certs import replacement_get_credentials_func from tests.clients.test_dapr_grpc_client import DaprGrpcClientTests from .fake_dapr_server import FakeDaprSidecar -def replacement_get_credentials_func(a): - """ - Used temporarily, so we can trust self-signed certificates in unit tests - until they get their own environment variable - """ - f = open(os.path.join(os.path.dirname(__file__), 'selfsigned.pem'), 'rb') - creds = grpc.ssl_channel_credentials(f.read()) - f.close() - - return creds - - DaprGrpcClient.get_credentials = replacement_get_credentials_func class DaprSecureGrpcClientTests(DaprGrpcClientTests): grpc_port = 50001 - http_port = 4443 + http_port = 3500 # The http server is used for health checks only, and doesn't need TLS scheme = 'https://' def setUp(self): self._fake_dapr_server = FakeDaprSidecar(grpc_port=self.grpc_port, http_port=self.http_port) + self._fake_dapr_server.start_secure() settings.DAPR_HTTP_PORT = self.http_port settings.DAPR_HTTP_ENDPOINT = 'http://127.0.0.1:{}'.format(self.http_port) - self._fake_dapr_server.start_secure() def tearDown(self): self._fake_dapr_server.stop_secure() From 169db4bca0c8f9bdf44736cbbe2c971d8c856273 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 12 Feb 2024 16:53:35 +0000 Subject: [PATCH 21/29] Unit tests passing Signed-off-by: Elena Kolevska --- .../test_secure_dapr_async_grpc_client.py | 10 ++++----- ...t_secure_http_service_invocation_client.py | 22 +++++-------------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/tests/clients/test_secure_dapr_async_grpc_client.py b/tests/clients/test_secure_dapr_async_grpc_client.py index eb540eff..d5baba4d 100644 --- a/tests/clients/test_secure_dapr_async_grpc_client.py +++ b/tests/clients/test_secure_dapr_async_grpc_client.py @@ -43,17 +43,17 @@ def setUp(self): def tearDown(self): self._fake_dapr_server.stop_secure() - @patch.object(settings, 'DAPR_GRPC_ENDPOINT', 'https://domain1.com:5000') + @patch.object(settings, 'DAPR_GRPC_ENDPOINT', 'dns:domain1.com:5000') def test_init_with_DAPR_GRPC_ENDPOINT(self): dapr = DaprGrpcClientAsync() self.assertEqual('dns:domain1.com:5000', dapr._uri.endpoint) - @patch.object(settings, 'DAPR_GRPC_ENDPOINT', 'https://domain1.com:5000') + @patch.object(settings, 'DAPR_GRPC_ENDPOINT', 'dns:domain1.com:5000') def test_init_with_DAPR_GRPC_ENDPOINT_and_argument(self): - dapr = DaprGrpcClientAsync('https://domain2.com:5002') + dapr = DaprGrpcClientAsync('dns:domain2.com:5002') self.assertEqual('dns:domain2.com:5002', dapr._uri.endpoint) - @patch.object(settings, 'DAPR_GRPC_ENDPOINT', 'https://domain1.com:5000') + @patch.object(settings, 'DAPR_GRPC_ENDPOINT', 'dns:domain1.com:5000') @patch.object(settings, 'DAPR_RUNTIME_HOST', 'domain2.com') @patch.object(settings, 'DAPR_GRPC_PORT', '5002') def test_init_with_DAPR_GRPC_ENDPOINT_and_DAPR_RUNTIME_HOST(self): @@ -63,7 +63,7 @@ def test_init_with_DAPR_GRPC_ENDPOINT_and_DAPR_RUNTIME_HOST(self): @patch.object(settings, 'DAPR_RUNTIME_HOST', 'domain1.com') @patch.object(settings, 'DAPR_GRPC_PORT', '5000') def test_init_with_argument_and_DAPR_GRPC_ENDPOINT_and_DAPR_RUNTIME_HOST(self): - dapr = DaprGrpcClientAsync('https://domain2.com:5002') + dapr = DaprGrpcClientAsync('dns:domain2.com:5002') self.assertEqual('dns:domain2.com:5002', dapr._uri.endpoint) async def test_dapr_api_token_insertion(self): diff --git a/tests/clients/test_secure_http_service_invocation_client.py b/tests/clients/test_secure_http_service_invocation_client.py index 528b078d..785353a5 100644 --- a/tests/clients/test_secure_http_service_invocation_client.py +++ b/tests/clients/test_secure_http_service_invocation_client.py @@ -29,23 +29,12 @@ from dapr.proto import common_v1 -from .certs import CERTIFICATE_CHAIN_PATH +from .certs import replacement_get_health_context from .fake_http_server import FakeHttpServer from .test_http_service_invocation_client import DaprInvocationHttpClientTests def replacement_get_client_ssl_context(a): - """ - This method is used (overwritten) from tests - to return context for self-signed certificates - """ - ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - ssl_context.load_verify_locations(CERTIFICATE_CHAIN_PATH) - - return ssl_context - - -def replacement_get_health_context(): """ This method is used (overwritten) from tests to return context for self-signed certificates @@ -57,17 +46,18 @@ def replacement_get_health_context(): return context +DaprHttpClient.get_ssl_context = replacement_get_client_ssl_context +DaprHealth.get_ssl_context = replacement_get_health_context + + class DaprSecureInvocationHttpClientTests(DaprInvocationHttpClientTests): def setUp(self): - DaprHttpClient.get_ssl_context = replacement_get_client_ssl_context - DaprHealth.get_context = replacement_get_health_context - self.server = FakeHttpServer(secure=True, port=4443) self.server_port = self.server.get_port() self.server.start() settings.DAPR_HTTP_PORT = self.server_port settings.DAPR_API_METHOD_INVOCATION_PROTOCOL = 'http' - settings.DAPR_HTTP_ENDPOINT = 'https://localhost:{}'.format(self.server_port) + settings.DAPR_HTTP_ENDPOINT = 'https://127.0.0.1:{}'.format(self.server_port) self.client = DaprClient() self.app_id = 'fakeapp' self.method_name = 'fakemethod' From ae56663cb06c76c4dde06caa8a0c798bc6f7335b Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 12 Feb 2024 16:54:53 +0000 Subject: [PATCH 22/29] Linter Signed-off-by: Elena Kolevska --- tests/clients/test_secure_dapr_async_grpc_client.py | 2 -- tests/clients/test_secure_dapr_grpc_client.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/tests/clients/test_secure_dapr_async_grpc_client.py b/tests/clients/test_secure_dapr_async_grpc_client.py index d5baba4d..fcc9f919 100644 --- a/tests/clients/test_secure_dapr_async_grpc_client.py +++ b/tests/clients/test_secure_dapr_async_grpc_client.py @@ -13,8 +13,6 @@ limitations under the License. """ -import os -import ssl import unittest from unittest.mock import patch diff --git a/tests/clients/test_secure_dapr_grpc_client.py b/tests/clients/test_secure_dapr_grpc_client.py index 017bbd36..74d4e644 100644 --- a/tests/clients/test_secure_dapr_grpc_client.py +++ b/tests/clients/test_secure_dapr_grpc_client.py @@ -12,10 +12,8 @@ See the License for the specific language governing permissions and limitations under the License. """ -import os import unittest from unittest.mock import patch -import grpc # type: ignore from dapr.clients.grpc.client import DaprGrpcClient from dapr.conf import settings From 60001ea52432049fcb06123a54748c9115d78859 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 12 Feb 2024 21:27:29 +0000 Subject: [PATCH 23/29] Refactor and cleanup Signed-off-by: Elena Kolevska --- dapr/clients/health.py | 4 +- examples/state_store/state_store.py | 3 +- ext/dapr-ext-fastapi/tests/test_app.py | 2 +- ext/flask_dapr/tests/test_app.py | 6 +- tests/clients/certs.py | 79 ++++++++++++------- tests/clients/fake_dapr_server.py | 29 +++---- tests/clients/fake_http_server.py | 32 ++++---- .../test_secure_dapr_async_grpc_client.py | 8 +- tests/clients/test_secure_dapr_grpc_client.py | 12 ++- ...t_secure_http_service_invocation_client.py | 4 +- 10 files changed, 99 insertions(+), 80 deletions(-) diff --git a/dapr/clients/health.py b/dapr/clients/health.py index 835f6c6d..f05bc118 100644 --- a/dapr/clients/health.py +++ b/dapr/clients/health.py @@ -37,8 +37,8 @@ def wait_until_ready(self): with urllib.request.urlopen(req, context=self.get_ssl_context()) as response: if 200 <= response.status < 300: break - except urllib.error.URLError as e: - print(f'Health check on {health_url} failed: {e.reason}') + except Exception as e: + print(f'Health check on {health_url} failed: {e}') remaining = (start + timeout) - time.time() if remaining <= 0: diff --git a/examples/state_store/state_store.py b/examples/state_store/state_store.py index 1213226e..f87167f5 100644 --- a/examples/state_store/state_store.py +++ b/examples/state_store/state_store.py @@ -34,7 +34,8 @@ # StatusCode should be StatusCode.ABORTED. print(f'Cannot save due to bad etag. ErrorCode={err.code()}') - # For detailed error messages from the dapr runtime: # print(f"Details={err.details()}) + # For detailed error messages from the dapr runtime: + # print(f"Details={err.details()}) # Save multiple states. d.save_bulk_state( diff --git a/ext/dapr-ext-fastapi/tests/test_app.py b/ext/dapr-ext-fastapi/tests/test_app.py index fa61fac7..831d55eb 100644 --- a/ext/dapr-ext-fastapi/tests/test_app.py +++ b/ext/dapr-ext-fastapi/tests/test_app.py @@ -1,8 +1,8 @@ import unittest from fastapi import FastAPI -from pydantic import BaseModel from fastapi.testclient import TestClient +from pydantic import BaseModel from dapr.ext.fastapi import DaprApp diff --git a/ext/flask_dapr/tests/test_app.py b/ext/flask_dapr/tests/test_app.py index 2665bb8a..7ddfa14f 100644 --- a/ext/flask_dapr/tests/test_app.py +++ b/ext/flask_dapr/tests/test_app.py @@ -1,8 +1,8 @@ -import unittest -from flask import Flask import json +import unittest -from flask_dapr import DaprApp # noqa: E402 +from flask import Flask +from flask_dapr import DaprApp class DaprAppTest(unittest.TestCase): diff --git a/tests/clients/certs.py b/tests/clients/certs.py index 12f3b57e..eecaf424 100644 --- a/tests/clients/certs.py +++ b/tests/clients/certs.py @@ -1,48 +1,66 @@ import os import ssl +import grpc from OpenSSL import crypto -import grpc -PRIVATE_KEY_PATH = os.path.join(os.path.dirname(__file__), 'private.key') -CERTIFICATE_CHAIN_PATH = os.path.join(os.path.dirname(__file__), 'selfsigned.pem') +class Certs(): + server_type = 'grpc' + + @classmethod + def create_certificates(cls): + # create a key pair + k = crypto.PKey() + k.generate_key(crypto.TYPE_RSA, 4096) + + # create a self-signed cert + cert = crypto.X509() + cert.get_subject().organizationName = 'Dapr' + cert.get_subject().commonName = 'localhost' + cert.gmtime_adj_notBefore(0) + cert.gmtime_adj_notAfter(24 * 60 * 60) + cert.set_issuer(cert.get_subject()) + cert.set_pubkey(k) + if cls.server_type == 'http': + cert.add_extensions([crypto.X509Extension(b'subjectAltName', False, b'DNS:localhost')]) -def create_certificates(server_type='grpc'): - # create a key pair - k = crypto.PKey() - k.generate_key(crypto.TYPE_RSA, 4096) + cert.sign(k, 'sha512') - # create a self-signed cert - cert = crypto.X509() - cert.get_subject().organizationName = 'Dapr' - cert.get_subject().commonName = 'localhost' - cert.gmtime_adj_notBefore(0) - cert.gmtime_adj_notAfter(24 * 60 * 60) - cert.set_issuer(cert.get_subject()) - cert.set_pubkey(k) + f_cert = open(cls.get_cert_path(), 'wt') + f_cert.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode('utf-8')) + f_cert.close() - if server_type == 'http': - cert.add_extensions([crypto.X509Extension(b'subjectAltName', False, b'DNS:localhost')]) + f_key = open(cls.get_pk_path(), 'wt') + f_key.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k).decode('utf-8')) + f_key.close() - cert.sign(k, 'sha512') + @classmethod + def get_pk_path(cls): + return os.path.join(os.path.dirname(__file__), '{}_private.key').format(cls.server_type) - f_cert = open(CERTIFICATE_CHAIN_PATH, 'wt') - f_cert.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode('utf-8')) - f_cert.close() + @classmethod + def get_cert_path(cls): + return os.path.join(os.path.dirname(__file__), '{}_selfsigned.pem').format(cls.server_type) - f_key = open(PRIVATE_KEY_PATH, 'wt') - f_key.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k).decode('utf-8')) - f_key.close() + @classmethod + def delete_certificates(cls): + pk = cls.get_pk_path() + if os.path.exists(pk): + os.remove(pk) + cert = cls.get_cert_path() + if os.path.exists(cert): + os.remove(cert) -def delete_certificates(): - if os.path.exists(PRIVATE_KEY_PATH): - os.remove(PRIVATE_KEY_PATH) - if os.path.exists(CERTIFICATE_CHAIN_PATH): - os.remove(CERTIFICATE_CHAIN_PATH) +class GrpcCerts(Certs): + server_type = 'grpc' + + +class HttpCerts(Certs): + server_type = 'http' def replacement_get_credentials_func(a): @@ -50,7 +68,8 @@ def replacement_get_credentials_func(a): Used temporarily, so we can trust self-signed certificates in unit tests until they get their own environment variable """ - f = open(os.path.join(os.path.dirname(__file__), 'selfsigned.pem'), 'rb') + + f = open(GrpcCerts.get_cert_path(), 'rb') creds = grpc.ssl_channel_credentials(f.read()) f.close() diff --git a/tests/clients/fake_dapr_server.py b/tests/clients/fake_dapr_server.py index 45ab4a36..70ee5502 100644 --- a/tests/clients/fake_dapr_server.py +++ b/tests/clients/fake_dapr_server.py @@ -1,3 +1,5 @@ +from ssl import SSLContext, PROTOCOL_TLS_SERVER + import grpc import json @@ -32,12 +34,7 @@ ) from typing import Dict -from tests.clients.certs import ( - create_certificates, - delete_certificates, - PRIVATE_KEY_PATH, - CERTIFICATE_CHAIN_PATH, -) +from tests.clients.certs import GrpcCerts from tests.clients.fake_http_server import FakeHttpServer @@ -46,7 +43,7 @@ def __init__(self, grpc_port: int = 50001, http_port: int = 8080): self.grpc_port = grpc_port self.http_port = http_port self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) - self._http_server = FakeHttpServer(self.http_port) + self._http_server = FakeHttpServer(self.http_port) # Needed for the healthcheck endpoint api_service_v1.add_DaprServicer_to_server(self, self._server) self.store = {} self.shutdown_received = False @@ -56,24 +53,21 @@ def __init__(self, grpc_port: int = 50001, http_port: int = 8080): self.metadata: Dict[str, str] = {} self._next_exception = None - def start( - self, - ): + def start(self, ): self._server.add_insecure_port(f'[::]:{self.grpc_port}') self._server.start() self._http_server.start() def start_secure(self): - create_certificates() + GrpcCerts.create_certificates() - private_key_file = open(PRIVATE_KEY_PATH, 'rb') + private_key_file = open(GrpcCerts.get_pk_path(), 'rb') private_key_content = private_key_file.read() private_key_file.close() - certificate_chain_file = open(CERTIFICATE_CHAIN_PATH, 'rb') + certificate_chain_file = open(GrpcCerts.get_cert_path(), 'rb') certificate_chain_content = certificate_chain_file.read() certificate_chain_file.close() - certificate_chain_file.close() credentials = grpc.ssl_server_credentials( [(private_key_content, certificate_chain_content)] @@ -81,9 +75,8 @@ def start_secure(self): self._server.add_secure_port(f'[::]:{self.grpc_port}', credentials) self._server.start() - # The http server is only needed for the healthcheck endpoint - # so it can be started without a certificate - self._http_server.start() + + self._http_server.start_secure() def stop(self): self._http_server.shutdown_server() @@ -92,7 +85,7 @@ def stop(self): def stop_secure(self): self._http_server.shutdown_server() self._server.stop(None) - delete_certificates() + GrpcCerts.delete_certificates() def raise_exception_on_next_call(self, exception): """ diff --git a/tests/clients/fake_http_server.py b/tests/clients/fake_http_server.py index 2aa5f39a..de9f5479 100644 --- a/tests/clients/fake_http_server.py +++ b/tests/clients/fake_http_server.py @@ -4,12 +4,7 @@ from threading import Thread from http.server import BaseHTTPRequestHandler, HTTPServer -from tests.clients.certs import ( - CERTIFICATE_CHAIN_PATH, - PRIVATE_KEY_PATH, - create_certificates, - delete_certificates, -) +from tests.clients.certs import (HttpCerts) class DaprHandler(BaseHTTPRequestHandler): @@ -53,19 +48,14 @@ def do_DELETE(self): class FakeHttpServer(Thread): - def __init__(self, port: int = 8080, secure=False): + secure = False + + def __init__(self, port: int = 8080): super().__init__() - self.secure = secure self.port = port self.server = HTTPServer(('localhost', self.port), DaprHandler) - if self.secure: - create_certificates('http') - ssl_context = SSLContext(PROTOCOL_TLS_SERVER) - ssl_context.load_cert_chain(CERTIFICATE_CHAIN_PATH, PRIVATE_KEY_PATH) - self.server.socket = ssl_context.wrap_socket(self.server.socket, server_side=True) - self.server.response_body = b'' self.server.response_code = 200 self.server.response_header_list = [] @@ -86,7 +76,7 @@ def shutdown_server(self): self.server.socket.close() self.join() if self.secure: - delete_certificates() + HttpCerts.delete_certificates() def request_path(self): return self.server.path @@ -101,5 +91,15 @@ def get_request_body(self): def set_server_delay(self, delay_seconds): self.server.sleep_time = delay_seconds + def start_secure(self): + self.secure = True + + HttpCerts.create_certificates() + ssl_context = SSLContext(PROTOCOL_TLS_SERVER) + ssl_context.load_cert_chain(HttpCerts.get_cert_path(), HttpCerts.get_pk_path()) + self.server.socket = ssl_context.wrap_socket(self.server.socket, server_side=True) + + self.start() + def run(self): - self.server.serve_forever() + self.server.serve_forever() \ No newline at end of file diff --git a/tests/clients/test_secure_dapr_async_grpc_client.py b/tests/clients/test_secure_dapr_async_grpc_client.py index fcc9f919..7db85ea2 100644 --- a/tests/clients/test_secure_dapr_async_grpc_client.py +++ b/tests/clients/test_secure_dapr_async_grpc_client.py @@ -18,25 +18,27 @@ from unittest.mock import patch from dapr.aio.clients.grpc.client import DaprGrpcClientAsync -from tests.clients.certs import replacement_get_credentials_func +from dapr.clients.health import DaprHealth +from tests.clients.certs import replacement_get_credentials_func, replacement_get_health_context from tests.clients.test_dapr_async_grpc_client import DaprGrpcClientAsyncTests from .fake_dapr_server import FakeDaprSidecar from dapr.conf import settings DaprGrpcClientAsync.get_credentials = replacement_get_credentials_func +DaprHealth.get_ssl_context = replacement_get_health_context class DaprSecureGrpcClientAsyncTests(DaprGrpcClientAsyncTests): grpc_port = 50001 - http_port = 3500 # The http server is used for health checks only, and doesn't need TLS + http_port = 4443 # The http server is used for health checks only, and doesn't need TLS scheme = 'https://' def setUp(self): self._fake_dapr_server = FakeDaprSidecar(grpc_port=self.grpc_port, http_port=self.http_port) self._fake_dapr_server.start_secure() settings.DAPR_HTTP_PORT = self.http_port - settings.DAPR_HTTP_ENDPOINT = 'http://127.0.0.1:{}'.format(self.http_port) + settings.DAPR_HTTP_ENDPOINT = 'https://127.0.0.1:{}'.format(self.http_port) def tearDown(self): self._fake_dapr_server.stop_secure() diff --git a/tests/clients/test_secure_dapr_grpc_client.py b/tests/clients/test_secure_dapr_grpc_client.py index 74d4e644..33d89835 100644 --- a/tests/clients/test_secure_dapr_grpc_client.py +++ b/tests/clients/test_secure_dapr_grpc_client.py @@ -16,26 +16,30 @@ from unittest.mock import patch from dapr.clients.grpc.client import DaprGrpcClient +from dapr.clients.health import DaprHealth from dapr.conf import settings -from tests.clients.certs import replacement_get_credentials_func +from tests.clients.certs import replacement_get_credentials_func, replacement_get_health_context from tests.clients.test_dapr_grpc_client import DaprGrpcClientTests from .fake_dapr_server import FakeDaprSidecar -DaprGrpcClient.get_credentials = replacement_get_credentials_func + class DaprSecureGrpcClientTests(DaprGrpcClientTests): grpc_port = 50001 - http_port = 3500 # The http server is used for health checks only, and doesn't need TLS + http_port = 4443 # The http server is used for health checks only, and doesn't need TLS scheme = 'https://' + DaprGrpcClient.get_credentials = replacement_get_credentials_func + DaprHealth.get_ssl_context = replacement_get_health_context + def setUp(self): self._fake_dapr_server = FakeDaprSidecar(grpc_port=self.grpc_port, http_port=self.http_port) self._fake_dapr_server.start_secure() settings.DAPR_HTTP_PORT = self.http_port - settings.DAPR_HTTP_ENDPOINT = 'http://127.0.0.1:{}'.format(self.http_port) + settings.DAPR_HTTP_ENDPOINT = 'https://127.0.0.1:{}'.format(self.http_port) def tearDown(self): self._fake_dapr_server.stop_secure() diff --git a/tests/clients/test_secure_http_service_invocation_client.py b/tests/clients/test_secure_http_service_invocation_client.py index 785353a5..95f4518d 100644 --- a/tests/clients/test_secure_http_service_invocation_client.py +++ b/tests/clients/test_secure_http_service_invocation_client.py @@ -52,9 +52,9 @@ def replacement_get_client_ssl_context(a): class DaprSecureInvocationHttpClientTests(DaprInvocationHttpClientTests): def setUp(self): - self.server = FakeHttpServer(secure=True, port=4443) + self.server = FakeHttpServer(port=4443) self.server_port = self.server.get_port() - self.server.start() + self.server.start_secure() settings.DAPR_HTTP_PORT = self.server_port settings.DAPR_API_METHOD_INVOCATION_PROTOCOL = 'http' settings.DAPR_HTTP_ENDPOINT = 'https://127.0.0.1:{}'.format(self.server_port) From 3ac1106d4f632ec475af03b113f221943c2fd0b4 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Tue, 13 Feb 2024 01:40:36 +0000 Subject: [PATCH 24/29] Refactors client tests for speed and readability Signed-off-by: Elena Kolevska --- tests/clients/certs.py | 21 ++++------ tests/clients/fake_dapr_server.py | 4 +- tests/clients/fake_http_server.py | 16 +++++++- tests/clients/test_dapr_grpc_client.py | 18 ++++---- ...ient.py => test_dapr_grpc_client_async.py} | 19 +++++---- ... => test_dapr_grpc_client_async_secure.py} | 18 ++++---- ...ent.py => test_dapr_grpc_client_secure.py} | 19 ++++----- tests/clients/test_exceptions.py | 22 +++++----- .../test_http_service_invocation_client.py | 30 ++++++++------ ...t_secure_http_service_invocation_client.py | 41 +++++++++++++------ 10 files changed, 120 insertions(+), 88 deletions(-) rename tests/clients/{test_dapr_async_grpc_client.py => test_dapr_grpc_client_async.py} (99%) rename tests/clients/{test_secure_dapr_async_grpc_client.py => test_dapr_grpc_client_async_secure.py} (86%) rename tests/clients/{test_secure_dapr_grpc_client.py => test_dapr_grpc_client_secure.py} (88%) diff --git a/tests/clients/certs.py b/tests/clients/certs.py index eecaf424..a30b2531 100644 --- a/tests/clients/certs.py +++ b/tests/clients/certs.py @@ -5,7 +5,7 @@ from OpenSSL import crypto -class Certs(): +class Certs: server_type = 'grpc' @classmethod @@ -28,13 +28,11 @@ def create_certificates(cls): cert.sign(k, 'sha512') - f_cert = open(cls.get_cert_path(), 'wt') - f_cert.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode('utf-8')) - f_cert.close() + with open(cls.get_cert_path(), 'wt') as f_cert: + f_cert.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode('utf-8')) - f_key = open(cls.get_pk_path(), 'wt') - f_key.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k).decode('utf-8')) - f_key.close() + with open(cls.get_pk_path(), 'wt') as f_key: + f_key.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k).decode('utf-8')) @classmethod def get_pk_path(cls): @@ -68,12 +66,9 @@ def replacement_get_credentials_func(a): Used temporarily, so we can trust self-signed certificates in unit tests until they get their own environment variable """ - - f = open(GrpcCerts.get_cert_path(), 'rb') - creds = grpc.ssl_channel_credentials(f.read()) - f.close() - - return creds + with open(GrpcCerts.get_cert_path(), 'rb') as f: + creds = grpc.ssl_channel_credentials(f.read()) + return creds def replacement_get_health_context(): diff --git a/tests/clients/fake_dapr_server.py b/tests/clients/fake_dapr_server.py index 70ee5502..74ca1e26 100644 --- a/tests/clients/fake_dapr_server.py +++ b/tests/clients/fake_dapr_server.py @@ -1,5 +1,3 @@ -from ssl import SSLContext, PROTOCOL_TLS_SERVER - import grpc import json @@ -53,7 +51,7 @@ def __init__(self, grpc_port: int = 50001, http_port: int = 8080): self.metadata: Dict[str, str] = {} self._next_exception = None - def start(self, ): + def start(self): self._server.add_insecure_port(f'[::]:{self.grpc_port}') self._server.start() self._http_server.start() diff --git a/tests/clients/fake_http_server.py b/tests/clients/fake_http_server.py index de9f5479..e08e82d2 100644 --- a/tests/clients/fake_http_server.py +++ b/tests/clients/fake_http_server.py @@ -4,7 +4,7 @@ from threading import Thread from http.server import BaseHTTPRequestHandler, HTTPServer -from tests.clients.certs import (HttpCerts) +from tests.clients.certs import HttpCerts class DaprHandler(BaseHTTPRequestHandler): @@ -15,6 +15,11 @@ def serve_forever(self): self.handle_request() def do_request(self, verb): + if self.path == '/v1.0/healthz/outbound': + self.send_response(200) + self.end_headers() + return + if self.server.sleep_time is not None: time.sleep(self.server.sleep_time) self.received_verb = verb @@ -102,4 +107,11 @@ def start_secure(self): self.start() def run(self): - self.server.serve_forever() \ No newline at end of file + self.server.serve_forever() + + def reset(self): + self.server.response_body = b'' + self.server.response_code = 200 + self.server.response_header_list = [] + self.server.request_body = b'' + self.server.sleep_time = None diff --git a/tests/clients/test_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client.py index 9e3ca04d..21f99a58 100644 --- a/tests/clients/test_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client.py @@ -47,14 +47,16 @@ class DaprGrpcClientTests(unittest.TestCase): scheme = '' error = None - def setUp(self): - self._fake_dapr_server = FakeDaprSidecar(grpc_port=self.grpc_port, http_port=self.http_port) - settings.DAPR_HTTP_PORT = self.http_port - settings.DAPR_HTTP_ENDPOINT = 'http://127.0.0.1:{}'.format(self.http_port) - self._fake_dapr_server.start() - - def tearDown(self): - self._fake_dapr_server.stop() + @classmethod + def setUpClass(cls): + cls._fake_dapr_server = FakeDaprSidecar(grpc_port=cls.grpc_port, http_port=cls.http_port) + cls._fake_dapr_server.start() + settings.DAPR_HTTP_PORT = cls.http_port + settings.DAPR_HTTP_ENDPOINT = 'http://127.0.0.1:{}'.format(cls.http_port) + + @classmethod + def tearDownClass(cls): + cls._fake_dapr_server.stop() def test_http_extension(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') diff --git a/tests/clients/test_dapr_async_grpc_client.py b/tests/clients/test_dapr_grpc_client_async.py similarity index 99% rename from tests/clients/test_dapr_async_grpc_client.py rename to tests/clients/test_dapr_grpc_client_async.py index 0039f174..95d7e405 100644 --- a/tests/clients/test_dapr_async_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client_async.py @@ -44,14 +44,17 @@ class DaprGrpcClientAsyncTests(unittest.IsolatedAsyncioTestCase): http_port = 3500 scheme = '' - def setUp(self): - self._fake_dapr_server = FakeDaprSidecar(grpc_port=self.grpc_port, http_port=self.http_port) - settings.DAPR_HTTP_PORT = self.http_port - settings.DAPR_HTTP_ENDPOINT = 'http://127.0.0.1:{}'.format(self.http_port) - self._fake_dapr_server.start() - - def tearDown(self): - self._fake_dapr_server.stop() + @classmethod + def setUpClass(cls): + cls._fake_dapr_server = FakeDaprSidecar(grpc_port=cls.grpc_port, http_port=cls.http_port) + cls._fake_dapr_server.start() + + settings.DAPR_HTTP_PORT = cls.http_port + settings.DAPR_HTTP_ENDPOINT = 'http://127.0.0.1:{}'.format(cls.http_port) + + @classmethod + def tearDownClass(cls): + cls._fake_dapr_server.stop() async def test_http_extension(self): dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') diff --git a/tests/clients/test_secure_dapr_async_grpc_client.py b/tests/clients/test_dapr_grpc_client_async_secure.py similarity index 86% rename from tests/clients/test_secure_dapr_async_grpc_client.py rename to tests/clients/test_dapr_grpc_client_async_secure.py index 7db85ea2..652feac2 100644 --- a/tests/clients/test_secure_dapr_async_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client_async_secure.py @@ -20,7 +20,7 @@ from dapr.aio.clients.grpc.client import DaprGrpcClientAsync from dapr.clients.health import DaprHealth from tests.clients.certs import replacement_get_credentials_func, replacement_get_health_context -from tests.clients.test_dapr_async_grpc_client import DaprGrpcClientAsyncTests +from tests.clients.test_dapr_grpc_client_async import DaprGrpcClientAsyncTests from .fake_dapr_server import FakeDaprSidecar from dapr.conf import settings @@ -34,14 +34,16 @@ class DaprSecureGrpcClientAsyncTests(DaprGrpcClientAsyncTests): http_port = 4443 # The http server is used for health checks only, and doesn't need TLS scheme = 'https://' - def setUp(self): - self._fake_dapr_server = FakeDaprSidecar(grpc_port=self.grpc_port, http_port=self.http_port) - self._fake_dapr_server.start_secure() - settings.DAPR_HTTP_PORT = self.http_port - settings.DAPR_HTTP_ENDPOINT = 'https://127.0.0.1:{}'.format(self.http_port) + @classmethod + def setUpClass(cls): + cls._fake_dapr_server = FakeDaprSidecar(grpc_port=cls.grpc_port, http_port=cls.http_port) + cls._fake_dapr_server.start_secure() + settings.DAPR_HTTP_PORT = cls.http_port + settings.DAPR_HTTP_ENDPOINT = 'https://127.0.0.1:{}'.format(cls.http_port) - def tearDown(self): - self._fake_dapr_server.stop_secure() + @classmethod + def tearDownClass(cls): + cls._fake_dapr_server.stop_secure() @patch.object(settings, 'DAPR_GRPC_ENDPOINT', 'dns:domain1.com:5000') def test_init_with_DAPR_GRPC_ENDPOINT(self): diff --git a/tests/clients/test_secure_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client_secure.py similarity index 88% rename from tests/clients/test_secure_dapr_grpc_client.py rename to tests/clients/test_dapr_grpc_client_secure.py index 33d89835..41dedca1 100644 --- a/tests/clients/test_secure_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client_secure.py @@ -24,9 +24,6 @@ from .fake_dapr_server import FakeDaprSidecar - - - class DaprSecureGrpcClientTests(DaprGrpcClientTests): grpc_port = 50001 http_port = 4443 # The http server is used for health checks only, and doesn't need TLS @@ -35,14 +32,16 @@ class DaprSecureGrpcClientTests(DaprGrpcClientTests): DaprGrpcClient.get_credentials = replacement_get_credentials_func DaprHealth.get_ssl_context = replacement_get_health_context - def setUp(self): - self._fake_dapr_server = FakeDaprSidecar(grpc_port=self.grpc_port, http_port=self.http_port) - self._fake_dapr_server.start_secure() - settings.DAPR_HTTP_PORT = self.http_port - settings.DAPR_HTTP_ENDPOINT = 'https://127.0.0.1:{}'.format(self.http_port) + @classmethod + def setUpClass(cls): + cls._fake_dapr_server = FakeDaprSidecar(grpc_port=cls.grpc_port, http_port=cls.http_port) + cls._fake_dapr_server.start_secure() + settings.DAPR_HTTP_PORT = cls.http_port + settings.DAPR_HTTP_ENDPOINT = 'https://127.0.0.1:{}'.format(cls.http_port) - def tearDown(self): - self._fake_dapr_server.stop_secure() + @classmethod + def tearDownClass(cls): + cls._fake_dapr_server.stop_secure() @patch.object(settings, 'DAPR_GRPC_ENDPOINT', 'https://domain1.com:5000') def test_init_with_DAPR_GRPC_ENDPOINT(self): diff --git a/tests/clients/test_exceptions.py b/tests/clients/test_exceptions.py index 313666af..fb349b09 100644 --- a/tests/clients/test_exceptions.py +++ b/tests/clients/test_exceptions.py @@ -93,17 +93,17 @@ class DaprExceptionsTestCase(unittest.TestCase): _grpc_port = 50001 _http_port = 3500 - def setUp(self): - self._fake_dapr_server = FakeDaprSidecar( - grpc_port=self._grpc_port, http_port=self._http_port - ) - settings.DAPR_HTTP_PORT = self._http_port - settings.DAPR_HTTP_ENDPOINT = 'http://127.0.0.1:{}'.format(self._http_port) - self._fake_dapr_server.start() - self._expected_status = create_expected_status() - - def tearDown(self): - self._fake_dapr_server.stop() + @classmethod + def setUpClass(cls): + cls._fake_dapr_server = FakeDaprSidecar(grpc_port=cls._grpc_port, http_port=cls._http_port) + settings.DAPR_HTTP_PORT = cls._http_port + settings.DAPR_HTTP_ENDPOINT = 'http://127.0.0.1:{}'.format(cls._http_port) + cls._fake_dapr_server.start() + cls._expected_status = create_expected_status() + + @classmethod + def tearDownClass(cls): + cls._fake_dapr_server.stop() def test_exception_status_parsing(self): dapr = DaprGrpcClient(f'localhost:{self._grpc_port}') diff --git a/tests/clients/test_http_service_invocation_client.py b/tests/clients/test_http_service_invocation_client.py index a5e16cfc..d45c530b 100644 --- a/tests/clients/test_http_service_invocation_client.py +++ b/tests/clients/test_http_service_invocation_client.py @@ -34,23 +34,29 @@ class DaprInvocationHttpClientTests(unittest.TestCase): - def setUp(self): - self.server = FakeHttpServer(port=3500) - self.server_port = self.server.get_port() - self.server.start() - settings.DAPR_HTTP_PORT = self.server_port - settings.DAPR_API_METHOD_INVOCATION_PROTOCOL = 'http' + server_port = 3500 - self.client = DaprClient() + @classmethod + def setUpClass(cls): + cls.server = FakeHttpServer(cls.server_port) + cls.server.start() + + cls.app_id = 'fakeapp' + cls.method_name = 'fakemethod' + cls.invoke_url = f'/v1.0/invoke/{cls.app_id}/method/{cls.method_name}' - self.app_id = 'fakeapp' - self.method_name = 'fakemethod' - self.invoke_url = f'/v1.0/invoke/{self.app_id}/method/{self.method_name}' + @classmethod + def tearDownClass(cls): + cls.server.shutdown_server() - def tearDown(self): - self.server.shutdown_server() + def setUp(self): settings.DAPR_API_TOKEN = None + settings.DAPR_HTTP_PORT = self.server_port settings.DAPR_API_METHOD_INVOCATION_PROTOCOL = 'http' + settings.DAPR_HTTP_ENDPOINT = 'http://127.0.0.1:{}'.format(self.server_port) + + self.server.reset() + self.client = DaprClient() def test_basic_invoke(self): self.server.set_response(b'STRING_BODY') diff --git a/tests/clients/test_secure_http_service_invocation_client.py b/tests/clients/test_secure_http_service_invocation_client.py index 95f4518d..f23bc11c 100644 --- a/tests/clients/test_secure_http_service_invocation_client.py +++ b/tests/clients/test_secure_http_service_invocation_client.py @@ -22,14 +22,14 @@ from opentelemetry.sdk.trace.sampling import ALWAYS_ON from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator -from dapr.clients import DaprClient +from dapr.clients import DaprClient, DaprGrpcClient from dapr.clients.health import DaprHealth from dapr.clients.http.client import DaprHttpClient from dapr.conf import settings from dapr.proto import common_v1 -from .certs import replacement_get_health_context +from .certs import replacement_get_health_context, replacement_get_credentials_func, GrpcCerts from .fake_http_server import FakeHttpServer from .test_http_service_invocation_client import DaprInvocationHttpClientTests @@ -47,31 +47,46 @@ def replacement_get_client_ssl_context(a): DaprHttpClient.get_ssl_context = replacement_get_client_ssl_context +DaprGrpcClient.get_credentials = replacement_get_credentials_func DaprHealth.get_ssl_context = replacement_get_health_context class DaprSecureInvocationHttpClientTests(DaprInvocationHttpClientTests): + server_port = 4443 + + @classmethod + def setUpClass(cls): + cls.server = FakeHttpServer(cls.server_port) + cls.server.start_secure() + + cls.app_id = 'fakeapp' + cls.method_name = 'fakemethod' + cls.invoke_url = f'/v1.0/invoke/{cls.app_id}/method/{cls.method_name}' + + # We need to set up the certificates for the gRPC server + # because the DaprGrpcClient will try to create a connection + GrpcCerts.create_certificates() + + @classmethod + def tearDownClass(cls): + GrpcCerts.delete_certificates() + cls.server.shutdown_server() + def setUp(self): - self.server = FakeHttpServer(port=4443) - self.server_port = self.server.get_port() - self.server.start_secure() + settings.DAPR_API_TOKEN = None settings.DAPR_HTTP_PORT = self.server_port settings.DAPR_API_METHOD_INVOCATION_PROTOCOL = 'http' settings.DAPR_HTTP_ENDPOINT = 'https://127.0.0.1:{}'.format(self.server_port) - self.client = DaprClient() - self.app_id = 'fakeapp' - self.method_name = 'fakemethod' - self.invoke_url = f'/v1.0/invoke/{self.app_id}/method/{self.method_name}' - def tearDown(self): - self.server.shutdown_server() - settings.DAPR_API_TOKEN = None - settings.DAPR_API_METHOD_INVOCATION_PROTOCOL = 'http' + self.server.reset() + self.client = DaprClient() def test_global_timeout_setting_is_honored(self): previous_timeout = settings.DAPR_HTTP_TIMEOUT_SECONDS settings.DAPR_HTTP_TIMEOUT_SECONDS = 1 + new_client = DaprClient(f'https://localhost:{self.server_port}') + self.server.set_server_delay(1.5) with self.assertRaises(TimeoutError): new_client.invoke_method(self.app_id, self.method_name, '') From 2aa2f32ce25f6482ea3a73a1a6f03b78a63afc7e Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Tue, 13 Feb 2024 01:48:23 +0000 Subject: [PATCH 25/29] Small fix Signed-off-by: Elena Kolevska --- dapr/clients/health.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dapr/clients/health.py b/dapr/clients/health.py index f05bc118..60297020 100644 --- a/dapr/clients/health.py +++ b/dapr/clients/health.py @@ -23,7 +23,7 @@ class DaprHealth: @classmethod - def wait_until_ready(self): + def wait_until_ready(cls): health_url = f'{get_api_url()}/healthz/outbound' headers = {USER_AGENT_HEADER: DAPR_USER_AGENT} if settings.DAPR_API_TOKEN is not None: @@ -34,11 +34,13 @@ def wait_until_ready(self): while True: try: req = urllib.request.Request(health_url, headers=headers) - with urllib.request.urlopen(req, context=self.get_ssl_context()) as response: + with urllib.request.urlopen(req, context=cls.get_ssl_context()) as response: if 200 <= response.status < 300: break + except urllib.error.URLError as e: + print(f'Health check on {health_url} failed: {e.reason}') except Exception as e: - print(f'Health check on {health_url} failed: {e}') + print(f'Unexpected error during health check: {e}') remaining = (start + timeout) - time.time() if remaining <= 0: From 7517cf70e3734ce6e8d1259a635760e57b099ea3 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Tue, 13 Feb 2024 02:12:03 +0000 Subject: [PATCH 26/29] Add tests performance improvement for actor tests too Signed-off-by: Elena Kolevska --- tests/actor/test_actor.py | 14 +++++++++----- tests/actor/test_actor_reentrancy.py | 15 +++++++++------ tests/actor/test_actor_runtime.py | 15 +++++++++------ 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/tests/actor/test_actor.py b/tests/actor/test_actor.py index d2944f16..e4264372 100644 --- a/tests/actor/test_actor.py +++ b/tests/actor/test_actor.py @@ -39,19 +39,23 @@ class ActorTests(unittest.TestCase): - def setUp(self): - self.server = FakeHttpServer(port=3500) - self.server.start() + @classmethod + def setUpClass(cls): + cls.server = FakeHttpServer(3500) + cls.server.start() settings.DAPR_HTTP_PORT = 3500 + @classmethod + def tearDownClass(cls): + cls.server.shutdown_server() + + def setUp(self): ActorRuntime._actor_managers = {} ActorRuntime.set_actor_config(ActorRuntimeConfig()) self._serializer = DefaultJSONSerializer() _run(ActorRuntime.register_actor(FakeSimpleActor)) _run(ActorRuntime.register_actor(FakeMultiInterfacesActor)) - def tearDown(self): - self.server.shutdown_server() def test_get_registered_actor_types(self): actor_types = ActorRuntime.get_registered_actor_types() diff --git a/tests/actor/test_actor_reentrancy.py b/tests/actor/test_actor_reentrancy.py index d2915900..834273f4 100644 --- a/tests/actor/test_actor_reentrancy.py +++ b/tests/actor/test_actor_reentrancy.py @@ -34,11 +34,17 @@ class ActorRuntimeTests(unittest.TestCase): - def setUp(self): - self.server = FakeHttpServer(port=3500) - self.server.start() + @classmethod + def setUpClass(cls): + cls.server = FakeHttpServer(3500) + cls.server.start() settings.DAPR_HTTP_PORT = 3500 + @classmethod + def tearDownClass(cls): + cls.server.shutdown_server() + + def setUp(self): ActorRuntime._actor_managers = {} ActorRuntime.set_actor_config( ActorRuntimeConfig(reentrancy=ActorReentrancyConfig(enabled=True)) @@ -48,9 +54,6 @@ def setUp(self): _run(ActorRuntime.register_actor(FakeSlowReentrantActor)) _run(ActorRuntime.register_actor(FakeMultiInterfacesActor)) - def tearDown(self): - self.server.shutdown_server() - def test_reentrant_dispatch(self): _run(ActorRuntime.register_actor(FakeMultiInterfacesActor)) diff --git a/tests/actor/test_actor_runtime.py b/tests/actor/test_actor_runtime.py index a38b30bc..f17f96cc 100644 --- a/tests/actor/test_actor_runtime.py +++ b/tests/actor/test_actor_runtime.py @@ -33,11 +33,17 @@ class ActorRuntimeTests(unittest.TestCase): - def setUp(self): - self.server = FakeHttpServer(port=3500) - self.server.start() + @classmethod + def setUpClass(cls): + cls.server = FakeHttpServer(3500) + cls.server.start() settings.DAPR_HTTP_PORT = 3500 + @classmethod + def tearDownClass(cls): + cls.server.shutdown_server() + + def setUp(self): ActorRuntime._actor_managers = {} ActorRuntime.set_actor_config(ActorRuntimeConfig()) self._serializer = DefaultJSONSerializer() @@ -45,9 +51,6 @@ def setUp(self): _run(ActorRuntime.register_actor(FakeMultiInterfacesActor)) _run(ActorRuntime.register_actor(FakeSimpleTimerActor)) - def tearDown(self): - self.server.shutdown_server() - def test_get_registered_actor_types(self): actor_types = ActorRuntime.get_registered_actor_types() self.assertTrue(actor_types.index('FakeSimpleActor') >= 0) From 94b23a013ab369d3ca8e614c3a6df7e0bb988095 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Tue, 13 Feb 2024 02:23:44 +0000 Subject: [PATCH 27/29] Cosmetic touch up Signed-off-by: Elena Kolevska --- tests/actor/test_actor.py | 1 - tests/clients/fake_dapr_server.py | 16 ++++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/actor/test_actor.py b/tests/actor/test_actor.py index e4264372..d9b602c9 100644 --- a/tests/actor/test_actor.py +++ b/tests/actor/test_actor.py @@ -56,7 +56,6 @@ def setUp(self): _run(ActorRuntime.register_actor(FakeSimpleActor)) _run(ActorRuntime.register_actor(FakeMultiInterfacesActor)) - def test_get_registered_actor_types(self): actor_types = ActorRuntime.get_registered_actor_types() self.assertTrue(actor_types.index('FakeSimpleActor') >= 0) diff --git a/tests/clients/fake_dapr_server.py b/tests/clients/fake_dapr_server.py index 74ca1e26..2de14984 100644 --- a/tests/clients/fake_dapr_server.py +++ b/tests/clients/fake_dapr_server.py @@ -40,9 +40,9 @@ class FakeDaprSidecar(api_service_v1.DaprServicer): def __init__(self, grpc_port: int = 50001, http_port: int = 8080): self.grpc_port = grpc_port self.http_port = http_port - self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + self._grpc_server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) self._http_server = FakeHttpServer(self.http_port) # Needed for the healthcheck endpoint - api_service_v1.add_DaprServicer_to_server(self, self._server) + api_service_v1.add_DaprServicer_to_server(self, self._grpc_server) self.store = {} self.shutdown_received = False self.locks_to_owner = {} # (store_name, resource_id) -> lock_owner @@ -52,8 +52,8 @@ def __init__(self, grpc_port: int = 50001, http_port: int = 8080): self._next_exception = None def start(self): - self._server.add_insecure_port(f'[::]:{self.grpc_port}') - self._server.start() + self._grpc_server.add_insecure_port(f'[::]:{self.grpc_port}') + self._grpc_server.start() self._http_server.start() def start_secure(self): @@ -71,18 +71,18 @@ def start_secure(self): [(private_key_content, certificate_chain_content)] ) - self._server.add_secure_port(f'[::]:{self.grpc_port}', credentials) - self._server.start() + self._grpc_server.add_secure_port(f'[::]:{self.grpc_port}', credentials) + self._grpc_server.start() self._http_server.start_secure() def stop(self): self._http_server.shutdown_server() - self._server.stop(None) + self._grpc_server.stop(None) def stop_secure(self): self._http_server.shutdown_server() - self._server.stop(None) + self._grpc_server.stop(None) GrpcCerts.delete_certificates() def raise_exception_on_next_call(self, exception): From 2a2f3bded0efdd3f588c9accb272323621c9fbc2 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Tue, 13 Feb 2024 02:27:09 +0000 Subject: [PATCH 28/29] Documents the `DAPR_HEALTH_TIMEOUT` environment variable Signed-off-by: Elena Kolevska --- daprdocs/content/en/python-sdk-docs/python-client.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/daprdocs/content/en/python-sdk-docs/python-client.md b/daprdocs/content/en/python-sdk-docs/python-client.md index 47f76c83..3030f64a 100644 --- a/daprdocs/content/en/python-sdk-docs/python-client.md +++ b/daprdocs/content/en/python-sdk-docs/python-client.md @@ -74,6 +74,14 @@ If your Dapr instance is configured to require the `DAPR_API_TOKEN` environment set it in the environment and the client will use it automatically. You can read more about Dapr API token authentication [here](https://docs.dapr.io/operations/security/api-token/). +##### Health timeout +On client initialisation, a health check is performed against the Dapr sidecar (`/healthz/outboud`). +The client will wait for the sidecar to be up and running before proceeding. + +The default timeout is 60 seconds, but it can be overridden by setting the `DAPR_HEALTH_TIMEOUT` +environment variable. + + ## Error handling Initially, errors in Dapr followed the [Standard gRPC error model](https://grpc.io/docs/guides/error/#standard-error-model). However, to provide more detailed and informative error messages, in version 1.13 an enhanced error model has been introduced which aligns with the gRPC [Richer error model](https://grpc.io/docs/guides/error/#richer-error-model). In response, the Python SDK implemented `DaprGrpcError`, a custom exception class designed to improve the developer experience. It's important to note that the transition to using `DaprGrpcError` for all gRPC status exceptions is a work in progress. As of now, not every API call in the SDK has been updated to leverage this custom exception. We are actively working on this enhancement and welcome contributions from the community. From 1e7f332fcab012286a868e79f8555c28cd78a565 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Tue, 13 Feb 2024 02:59:29 +0000 Subject: [PATCH 29/29] make healthcheck a static method Signed-off-by: Elena Kolevska --- dapr/clients/health.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dapr/clients/health.py b/dapr/clients/health.py index 60297020..120b5593 100644 --- a/dapr/clients/health.py +++ b/dapr/clients/health.py @@ -22,8 +22,8 @@ class DaprHealth: - @classmethod - def wait_until_ready(cls): + @staticmethod + def wait_until_ready(): health_url = f'{get_api_url()}/healthz/outbound' headers = {USER_AGENT_HEADER: DAPR_USER_AGENT} if settings.DAPR_API_TOKEN is not None: @@ -34,7 +34,7 @@ def wait_until_ready(cls): while True: try: req = urllib.request.Request(health_url, headers=headers) - with urllib.request.urlopen(req, context=cls.get_ssl_context()) as response: + with urllib.request.urlopen(req, context=DaprHealth.get_ssl_context()) as response: if 200 <= response.status < 300: break except urllib.error.URLError as e: @@ -47,8 +47,8 @@ def wait_until_ready(cls): raise TimeoutError(f'Dapr health check timed out, after {timeout}.') time.sleep(min(1, remaining)) - @classmethod - def get_ssl_context(cls): + @staticmethod + def get_ssl_context(): # This method is used (overwritten) from tests # to return context for self-signed certificates return None