Skip to content

Commit

Permalink
Dev/19: ServiceInstanceListSupplierBuilder design basic implementation (
Browse files Browse the repository at this point in the history
#24)

* 🍺 implement DiscoveryClient design

* Add more comments

* Modify some functional operation concerning performance

* 📝 add typing and docstrings

* Make some fields private

* Fix naming

* pending discussion: next+filter vs list comprehension

* 🎨 Add type checking syntax

* improve filter_get_first utils

* ✨ FixedServiceInstanceListSupplier completed

* ✨ CachingServiceInstanceListSupplier completed

* 🎨 Add type checking syntax

* 🎨 Add NoneTypeError and some type checking

* 🍺 implement DiscoveryClient design

* Add more comments

* Modify some functional operation concerning performance

* 📝 add typing and docstrings

* Make some fields private

* Fix naming

* pending discussion: next+filter vs list comprehension

* 🎨 Add type checking syntax

* improve filter_get_first utils

* 🚚 Modify the package layout, rooted from spring_cloud

* add utils/validate
  • Loading branch information
Johnny850807 authored Nov 13, 2020
1 parent ec70989 commit 7fb4066
Show file tree
Hide file tree
Showing 14 changed files with 328 additions and 1 deletion.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ python = "^3.7"
pre-commit = "^2.7.1"
pytest = "^6.1.2"
pytest-cov = "^2.10.1"
pytest-mock = "^3.3.1"

[build-system]
requires = ["poetry>=0.12"]
Expand Down
3 changes: 3 additions & 0 deletions spring_cloud/commons/client/loadbalancer/supplier/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from .base import *
from .decorator import *
85 changes: 85 additions & 0 deletions spring_cloud/commons/client/loadbalancer/supplier/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
"""
Since the load-balancer is responsible for choosing one instance
per service request from a list of instances. We need a ServiceInstanceListSupplier for
each service to decouple the source of the instances from load-balancers.
"""
# standard library
from abc import ABC, abstractmethod
from typing import List

# scip plugin
from spring_cloud.commons.client import ServiceInstance
from spring_cloud.commons.client.discovery import DiscoveryClient

__author__ = "Waterball ([email protected])"
__license__ = "Apache 2.0"


class ServiceInstanceListSupplier(ABC):
"""
Non-Reactive version of ServiceInstanceListSupplier.
(Spring Cloud implement the supplier in the reactive way, means that
its supplier returns an Observable which broadcasts the instances on every change.)
We may consider to adopt reactive programming in the future.
"""

@property
@abstractmethod
def service_id(self) -> str:
"""
:return: (str) the service's id
"""
pass

@abstractmethod
def get(self, request=None) -> List[ServiceInstance]:
"""
:param request (opt) TODO not sure will we need this,
this extension was designed by spring-cloud.
:return: (*ServiceInstance) a list of instances
"""
pass


class FixedServiceInstanceListSupplier(ServiceInstanceListSupplier):
"""
A supplier that is initialized with fixed instances. (i.e. they won't be changed)
"""

def __init__(self, service_id: str, instances: List[ServiceInstance]):
"""
:param service_id: (str)
:param instances: (*ServiceInstance)
"""
self._service_id = service_id
self._instances = instances

def get(self, request=None) -> List[ServiceInstance]:
return self._instances

@property
def service_id(self) -> str:
return self._service_id


class DiscoveryClientServiceInstanceListSupplier(ServiceInstanceListSupplier):
"""
The adapter delegating to discovery client for querying instances
"""

def __init__(self, service_id: str, discovery_client: DiscoveryClient):
"""
:param service_id: (str)
:param discovery_client: (DiscoveryClient)
"""
self.__service_id = service_id
self.__delegate = discovery_client

@property
def service_id(self) -> str:
return self.__service_id

def get(self, request=None) -> List[ServiceInstance]:
return self.__delegate.get_instances(self.service_id)
43 changes: 43 additions & 0 deletions spring_cloud/commons/client/loadbalancer/supplier/decorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# standard library
from typing import List

# scip plugin
from spring_cloud.commons.client import ServiceInstance
from spring_cloud.commons.exceptions import NoneTypeError
from spring_cloud.commons.utils import validate

__author__ = "Waterball ([email protected])"
__license__ = "Apache 2.0"
# standard library
from abc import ABC

# scip plugin
from spring_cloud.commons.helpers import CacheManager

from .base import ServiceInstanceListSupplier


class DelegatingServiceInstanceListSupplier(ServiceInstanceListSupplier, ABC):
"""
An application of decorator pattern that adds behaviors to ServiceInstanceListSupplier.
The decorators should inherit this class.
"""

def __init__(self, delegate: ServiceInstanceListSupplier):
self.delegate = validate.not_none(delegate)

@property
def service_id(self) -> str:
return self.delegate.service_id


class CachingServiceInstanceListSupplier(DelegatingServiceInstanceListSupplier):
CACHE_NAME = "CacheKey"

def __init__(self, cache_manager: CacheManager, delegate: ServiceInstanceListSupplier):
super().__init__(delegate)
self.__cache_manager = cache_manager

def get(self, request=None) -> List[ServiceInstance]:
return self.__cache_manager.get(self.CACHE_NAME).on_cache_miss(self.delegate.get)
15 changes: 14 additions & 1 deletion spring_cloud/commons/client/service_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
__author__ = "Waterball ([email protected])"
__license__ = "Apache 2.0"


# standard library
from abc import ABC, abstractmethod
from urllib.parse import urlparse
Expand Down Expand Up @@ -95,3 +94,17 @@ def uri(self) -> str:
@property
def scheme(self) -> str:
return self._scheme

def __eq__(self, o: object) -> bool:
if isinstance(o, ServiceInstance):
return (
self.uri == o.uri
and self.service_id == o.service_id
and self.instance_id == o.instance_id
and self.host == o.host
and self.port == o.port
and self.secure == o.secure
and self.scheme == o.scheme
)

return False
3 changes: 3 additions & 0 deletions spring_cloud/commons/exceptions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-

from .primitive import *
17 changes: 17 additions & 0 deletions spring_cloud/commons/exceptions/primitive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
"""
All primitive exceptions are included here
"""

__author__ = "Waterball ([email protected])"
__license__ = "Apache 2.0"


class NoneTypeError(Exception):
@staticmethod
def raise_if_none(obj):
if obj is None:
raise NoneTypeError

def __init__(self, message):
self.message = message
2 changes: 2 additions & 0 deletions spring_cloud/commons/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from .cache import *
3 changes: 3 additions & 0 deletions spring_cloud/commons/helpers/cache/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-

from .cache_manager import *
69 changes: 69 additions & 0 deletions spring_cloud/commons/helpers/cache/cache_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-

"""
A cache manager wrapper that supports some syntax sugar.
Usage:
value = cache_manager.get(cache_key) \
.on_cache_miss(lambda: retrieve_value(key))
"""


__author__ = "Waterball ([email protected])"
__license__ = "Apache 2.0"

# standard library
from abc import ABC, abstractmethod


class OnCacheMiss:
def __init__(self, cache_manager, key, value):
self.__cache_manager = cache_manager
self.__key = key
self.__value = value

@abstractmethod
def on_cache_miss(self, cache_miss_func):
"""
:param cache_miss_func: (lambda ()->value)
"""
if not self.__value:
value = cache_miss_func()
self.__cache_manager.put(self.__key, value)
return value
return self.__value


class CacheManager(ABC):
"""
Service Provider Interface (SPI) for basic caching.
We might want to extend this class with many features in the future.
(e.g. timeout, evict-and-replacement)
"""

def get(self, key) -> OnCacheMiss:
value = self.retrieve_value(key)
return OnCacheMiss(self, key, value)

@abstractmethod
def retrieve_value(self, key):
pass

@abstractmethod
def put(self, key, value):
pass


class NaiveCacheManager(CacheManager):
"""
A very simple cache implementation.
"""

def __init__(self):
self.cache_dict = {}

def retrieve_value(self, key):
return self.cache_dict.get(key, None)

def put(self, key, value):
self.cache_dict[key] = value
18 changes: 18 additions & 0 deletions spring_cloud/commons/utils/validate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# scip plugin
from spring_cloud.commons.exceptions.primitive import NoneTypeError

__author__ = "Waterball ([email protected])"
__license__ = "Apache 2.0"


def not_none(obj):
if obj:
return obj
raise NoneTypeError


def is_instance_of(obj, the_type):
if isinstance(obj, the_type):
return obj
raise TypeError()
23 changes: 23 additions & 0 deletions tests/commons/client/loadbalancer/supplier/decorator_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# standard library
from unittest.mock import Mock

# scip plugin
from spring_cloud.commons.client.loadbalancer.supplier.decorator import CachingServiceInstanceListSupplier
from spring_cloud.commons.helpers.cache.cache_manager import NaiveCacheManager
from tests.commons.client.loadbalancer.supplier.stubs import INSTANCES

__author__ = "Waterball ([email protected])"
__license__ = "Apache 2.0"


class TestCachingServiceInstanceListSupplier:
def setup_class(self):
self.delegate = Mock()
self.delegate.get = Mock(return_value=INSTANCES)
self.supplier = CachingServiceInstanceListSupplier(NaiveCacheManager(), self.delegate)

def test_Given_cache_When_10_invocations_Then_only_1_cache_miss_and_delegate(self):
for i in range(1, 10):
self.supplier.get()
assert self.delegate.get.call_count == 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# standard library
from unittest.mock import Mock

# scip plugin
from spring_cloud.commons.client.loadbalancer.supplier import (
DiscoveryClientServiceInstanceListSupplier,
FixedServiceInstanceListSupplier,
)
from tests.commons.client.loadbalancer.supplier.stubs import INSTANCES, SERVICE_ID

__author__ = "Waterball ([email protected])"
__license__ = "Apache 2.0"


class TestFixedServiceInstanceListSupplier:
def setup_class(self):
self.supplier = FixedServiceInstanceListSupplier(SERVICE_ID, INSTANCES)

def test_get_service_id(self):
assert SERVICE_ID == self.supplier.service_id

def test_get_instances(self):
assert INSTANCES == self.supplier.get()


class TestDiscoveryClientServiceInstanceListSupplier:
def setup_class(self):
self.discovery_client = Mock()
self.discovery_client.get_instances = Mock(return_value=INSTANCES)
self.supplier = DiscoveryClientServiceInstanceListSupplier(SERVICE_ID, self.discovery_client)

def test_get_service_id(self):
assert SERVICE_ID == self.supplier.service_id

def test_get_instances(self):
assert INSTANCES == self.supplier.get()
10 changes: 10 additions & 0 deletions tests/commons/client/loadbalancer/supplier/stubs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# scip plugin
from spring_cloud.commons.client import StaticServiceInstance

__author__ = "Waterball ([email protected])"
__license__ = "Apache 2.0"


SERVICE_ID = "serviceId"
INSTANCES = [StaticServiceInstance("uri", SERVICE_ID, instance_id) for instance_id in ["1", "2", "3"]]

0 comments on commit 7fb4066

Please sign in to comment.