|
|
|
@ -1,50 +1,126 @@ |
|
|
|
|
# TODO: Flowerbox. |
|
|
|
|
|
|
|
|
|
import collections |
|
|
|
|
import datetime |
|
|
|
|
import os |
|
|
|
|
import logging |
|
|
|
|
import threading |
|
|
|
|
|
|
|
|
|
import grpc |
|
|
|
|
from typing import Any, Callable, Optional, Sequence, Text, Tuple, Union |
|
|
|
|
|
|
|
|
|
_CHANNEL_CACHE = None |
|
|
|
|
_CHANNEL_CACHE_LOCK = threading.RLock() |
|
|
|
|
|
|
|
|
|
# TODO: Evict channels. |
|
|
|
|
_LOGGER = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
|
_EVICTION_PERIOD_KEY = "GRPC_PYTHON_MANAGED_CHANNEL_EVICTION_SECONDS" |
|
|
|
|
if _EVICTION_PERIOD_KEY in os.environ: |
|
|
|
|
_EVICTION_PERIOD = datetime.timedelta(seconds=float(os.environ[_EVICTION_PERIOD_KEY])) |
|
|
|
|
else: |
|
|
|
|
_EVICTION_PERIOD = datetime.timedelta(minutes=10) |
|
|
|
|
|
|
|
|
|
# Eviction policy based on both channel count and time since use. Perhaps |
|
|
|
|
# OrderedDict instead? |
|
|
|
|
|
|
|
|
|
def _create_channel(target: Text, |
|
|
|
|
options: Sequence[Tuple[Text, Text]], |
|
|
|
|
channel_credentials: Optional[grpc.ChannelCredentials], |
|
|
|
|
compression: Optional[grpc.Compression]) -> grpc.Channel: |
|
|
|
|
if channel_credentials is None: |
|
|
|
|
_LOGGER.info(f"Creating insecure channel with options '{options}' " + |
|
|
|
|
f"and compression '{compression}'") |
|
|
|
|
return grpc.insecure_channel(target, |
|
|
|
|
options=options, |
|
|
|
|
compression=compression) |
|
|
|
|
else: |
|
|
|
|
_LOGGER.info(f"Creating secure channel with credentials '{channel_credentials}', " + |
|
|
|
|
f"options '{options}' and compression '{compression}'") |
|
|
|
|
return grpc.secure_channel(target, |
|
|
|
|
credentials=channel_credentials, |
|
|
|
|
options=options, |
|
|
|
|
compression=compression) |
|
|
|
|
|
|
|
|
|
class ChannelCache: |
|
|
|
|
_singleton = None |
|
|
|
|
_lock = threading.RLock() |
|
|
|
|
_condition = threading.Condition(lock=_lock) |
|
|
|
|
_eviction_ready = threading.Event() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self): |
|
|
|
|
self._mapping = collections.OrderedDict() |
|
|
|
|
self._eviction_thread = threading.Thread(target=ChannelCache._perform_evictions, daemon=True) |
|
|
|
|
self._eviction_thread.start() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod |
|
|
|
|
def get(): |
|
|
|
|
with ChannelCache._lock: |
|
|
|
|
if ChannelCache._singleton is None: |
|
|
|
|
ChannelCache._singleton = ChannelCache() |
|
|
|
|
ChannelCache._eviction_ready.wait() |
|
|
|
|
return ChannelCache._singleton |
|
|
|
|
|
|
|
|
|
# TODO: Type annotate key. |
|
|
|
|
def _evict_locked(self, key): |
|
|
|
|
channel, _ = self._mapping.pop(key) |
|
|
|
|
_LOGGER.info(f"Evicting channel {channel} with configuration {key}.") |
|
|
|
|
channel.close() |
|
|
|
|
del channel |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# TODO: Refactor. Way too deeply nested. |
|
|
|
|
@staticmethod |
|
|
|
|
def _perform_evictions(): |
|
|
|
|
while True: |
|
|
|
|
with ChannelCache._lock: |
|
|
|
|
ChannelCache._eviction_ready.set() |
|
|
|
|
if not ChannelCache._singleton._mapping: |
|
|
|
|
ChannelCache._condition.wait() |
|
|
|
|
else: |
|
|
|
|
key, (channel, eviction_time) = next(iter(ChannelCache._singleton._mapping.items())) |
|
|
|
|
now = datetime.datetime.now() |
|
|
|
|
if eviction_time <= now: |
|
|
|
|
ChannelCache._singleton._evict_locked(key) |
|
|
|
|
continue |
|
|
|
|
else: |
|
|
|
|
time_to_eviction = (eviction_time - now).total_seconds() |
|
|
|
|
ChannelCache._condition.wait(timeout=time_to_eviction) |
|
|
|
|
|
|
|
|
|
def _get_cached_channel(target: Text, |
|
|
|
|
options: Sequence[Tuple[Text, Text]], |
|
|
|
|
channel_credentials: Optional[grpc.ChannelCredentials], |
|
|
|
|
compression: Optional[grpc.Compression]) -> grpc.Channel: |
|
|
|
|
global _CHANNEL_CACHE |
|
|
|
|
global _CHANNEL_CACHE_LOCK |
|
|
|
|
key = (target, options, channel_credentials, compression) |
|
|
|
|
with _CHANNEL_CACHE_LOCK: |
|
|
|
|
if _CHANNEL_CACHE is None: |
|
|
|
|
_CHANNEL_CACHE = {} |
|
|
|
|
channel = _CHANNEL_CACHE.get(key, None) |
|
|
|
|
if channel is not None: |
|
|
|
|
return channel |
|
|
|
|
else: |
|
|
|
|
channel = _create_channel(target, options, channel_credentials, compression) |
|
|
|
|
_CHANNEL_CACHE[key] = channel |
|
|
|
|
return channel |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_channel(self, |
|
|
|
|
target: Text, |
|
|
|
|
options: Sequence[Tuple[Text, Text]], |
|
|
|
|
channel_credentials: Optional[grpc.ChannelCredentials], |
|
|
|
|
compression: Optional[grpc.Compression]) -> grpc.Channel: |
|
|
|
|
key = (target, options, channel_credentials, compression) |
|
|
|
|
with self._lock: |
|
|
|
|
# TODO: Can the get and the pop be turned into a single operation? |
|
|
|
|
channel_data = self._mapping.get(key, None) |
|
|
|
|
if channel_data is not None: |
|
|
|
|
channel = channel_data[0] |
|
|
|
|
# # NOTE: This isn't actually necessary. The eviction thread will |
|
|
|
|
# # always wake up because the new head of the list will, by |
|
|
|
|
# # definition, have a later eviction time than the old head of |
|
|
|
|
# # the list. If however, we allow for channels with heterogeneous |
|
|
|
|
# # eviction periods, this *will* become necessary. We can imagine |
|
|
|
|
# # this would be the case for timeouts. That is, if a timeout |
|
|
|
|
# # longer than the eviction period is specified, we do not want |
|
|
|
|
# # to cancel the RPC prematurely. |
|
|
|
|
# if channel is next(iter(self._mapping.values()))[0]: |
|
|
|
|
# self._condition.notify() |
|
|
|
|
# Move to the end of the map. |
|
|
|
|
self._mapping.pop(key) |
|
|
|
|
self._mapping[key] = (channel, datetime.datetime.now() + _EVICTION_PERIOD) |
|
|
|
|
return channel |
|
|
|
|
else: |
|
|
|
|
channel = _create_channel(target, options, channel_credentials, compression) |
|
|
|
|
self._mapping[key] = (channel, datetime.datetime.now() + _EVICTION_PERIOD) |
|
|
|
|
if len(self._mapping) == 1: |
|
|
|
|
self._condition.notify() |
|
|
|
|
return channel |
|
|
|
|
|
|
|
|
|
def _test_only_channel_count(self) -> int: |
|
|
|
|
with self._lock: |
|
|
|
|
return len(self._mapping) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def unary_unary(request: Any, |
|
|
|
@ -58,17 +134,21 @@ def unary_unary(request: Any, |
|
|
|
|
call_credentials: Optional[grpc.CallCredentials] = None, |
|
|
|
|
compression: Optional[grpc.Compression] = None, |
|
|
|
|
wait_for_ready: Optional[bool] = None, |
|
|
|
|
timeout: Optional[float] = None, |
|
|
|
|
metadata: Optional[Sequence[Tuple[Text, Union[Text, bytes]]]] = None) -> Any: |
|
|
|
|
"""Invokes a unary RPC without an explicitly specified channel. |
|
|
|
|
|
|
|
|
|
This is backed by an LRU cache of channels evicted by a background thread |
|
|
|
|
This is backed by a cache of channels evicted by a background thread |
|
|
|
|
on a periodic basis. |
|
|
|
|
|
|
|
|
|
TODO: Document the parameters and return value. |
|
|
|
|
""" |
|
|
|
|
channel = _get_cached_channel(target, options, channel_credentials, compression) |
|
|
|
|
|
|
|
|
|
# TODO: Warn if the timeout is greater than the channel eviction time. |
|
|
|
|
channel = ChannelCache.get().get_channel(target, options, channel_credentials, compression) |
|
|
|
|
multicallable = channel.unary_unary(method, request_serializer, request_deserializer) |
|
|
|
|
return multicallable(request, |
|
|
|
|
metadata=metadata, |
|
|
|
|
wait_for_ready=wait_for_ready, |
|
|
|
|
credentials=call_credentials) |
|
|
|
|
credentials=call_credentials, |
|
|
|
|
timeout=timeout) |
|
|
|
|