Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev/19: ServiceInstanceListSupplierBuilder design basic implementation #24

Merged
merged 27 commits into from
Nov 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
734a61a
:beer: implement DiscoveryClient design
Johnny850807 Nov 5, 2020
a94c3b3
Add more comments
Johnny850807 Nov 5, 2020
fc2dbb4
Modify some functional operation concerning performance
Johnny850807 Nov 6, 2020
02c1d47
:memo: add typing and docstrings
Johnny850807 Nov 6, 2020
617c03e
Make some fields private
Johnny850807 Nov 6, 2020
cdfa9a7
Fix naming
Johnny850807 Nov 6, 2020
70c8aa1
pending discussion: next+filter vs list comprehension
Johnny850807 Nov 6, 2020
41a8c64
:art: Add type checking syntax
Johnny850807 Nov 6, 2020
b059864
improve filter_get_first utils
Johnny850807 Nov 6, 2020
3115215
:sparkles: FixedServiceInstanceListSupplier completed
Johnny850807 Nov 6, 2020
560fe21
:sparkles: CachingServiceInstanceListSupplier completed
Johnny850807 Nov 6, 2020
72d9118
:art: Add type checking syntax
Johnny850807 Nov 6, 2020
464b9dd
:art: Add NoneTypeError and some type checking
Johnny850807 Nov 6, 2020
0b54017
:beer: implement DiscoveryClient design
Johnny850807 Nov 5, 2020
83a45fe
Add more comments
Johnny850807 Nov 5, 2020
d65d60d
Modify some functional operation concerning performance
Johnny850807 Nov 6, 2020
37e1afe
:memo: add typing and docstrings
Johnny850807 Nov 6, 2020
b2bb27b
Make some fields private
Johnny850807 Nov 6, 2020
3c1a014
Fix naming
Johnny850807 Nov 6, 2020
fbc9a10
pending discussion: next+filter vs list comprehension
Johnny850807 Nov 6, 2020
5510384
:art: Add type checking syntax
Johnny850807 Nov 6, 2020
cdd9e19
improve filter_get_first utils
Johnny850807 Nov 6, 2020
1ed2a37
Conflict solved
Johnny850807 Nov 8, 2020
bae830a
:truck: Modify the package layout, rooted from spring_cloud
Johnny850807 Nov 9, 2020
56b58bf
:truck: Modify the package layout, rooted from spring_cloud
Johnny850807 Nov 9, 2020
9a7db39
Solve conflict
Johnny850807 Nov 12, 2020
2718ef0
add utils/validate
Johnny850807 Nov 12, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"]]