Skip to content

Commit 03f8b3d

Browse files
authoredJun 7, 2023
Fix to timeout server stream if remote endpoint isn't available. (#10)
* Fix to timeout server stream if remote endpoint isn't available.
1 parent 9a669db commit 03f8b3d

22 files changed

+440
-248
lines changed
 

‎.github/workflows/validate.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ jobs:
1616
strategy:
1717
fail-fast: false
1818
matrix:
19-
python-version: ["3.10.x", "3.11.x"]
20-
poetry-version: ["1.4.2"]
19+
python-version: ["3.11.x"]
20+
poetry-version: ["1.5.0"]
2121
os: [ubuntu-latest]
2222
runs-on: ${{ matrix.os }}
2323
steps:

‎.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ repos:
2626
- id: black
2727

2828
- repo: https://github.com/pre-commit/mirrors-mypy
29-
rev: v1.2.0
29+
rev: v1.3.0
3030
hooks:
3131
- id: mypy
3232

‎DEVELOPING.md

+8-8
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ http://oss.oracle.com/licenses/upl.
99

1010
### Pre-Requisites
1111
* Use `bash` shell
12-
* [pyenv](https://github.com/pyenv/pyenv) version 2.2.4 or later
13-
* [poetry](https://github.com/python-poetry/poetry) version 1.1.13 or later
12+
* [pyenv](https://github.com/pyenv/pyenv) version 2.3.x or later
13+
* [poetry](https://github.com/python-poetry/poetry) version 1.5.x or later
1414

1515
### Project Structure
1616
* `bin` - various shell scripts
@@ -19,18 +19,18 @@ http://oss.oracle.com/licenses/upl.
1919
* `tests` - contains the library test cases in plain Python
2020

2121
### Setup working environment
22-
1. Install Python version 3.10.1
22+
1. Install Python version 3.11.3
2323

24-
```pyenv install 3.10.1```
24+
```pyenv install 3.11.3```
2525

2626
2. Checkout the source from GitHub and change directory to the project root dir
2727

28-
3. Set pyenv local version for this project to 3.10.1
28+
3. Set pyenv local version for this project to 3.11.3
2929

30-
```pyenv local 3.10.1```
31-
4. Set poetry to use python 3.10.1
30+
```pyenv local 3.11.3```
31+
4. Set poetry to use python 3.11.3
3232

33-
```poetry env use 3.10.1```
33+
```poetry env use 3.11.3```
3434
5. Install all the required dependencies using poetry
3535

3636
```poetry install```

‎README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ The Coherence Python Client allows Python applications to act as cache clients t
2020

2121
#### Requirements
2222
* [Coherence CE](https://github.com/oracle/coherence) 22.06.4+ or Coherence 14.1.1.2206.4+ Commercial edition with a configured [gRPCProxy](https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.2206/develop-remote-clients/using-coherence-grpc-server.html).
23-
* Python 3.10.1
23+
* Python 3.11.x
24+
2425

2526
#### Starting a Coherence Cluster
2627

@@ -56,7 +57,7 @@ import asyncio
5657
async def run_test():
5758

5859
# create a new Session to the Coherence server
59-
session: Session = Session(None)
60+
session: Session = await Session.create()
6061

6162
# create a new NamedCache with key of string|int and value of string|int
6263
cache: NamedCache[str, str|int] = await session.get_cache("test")

‎examples/aggregators.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ async def do_run() -> None:
3636
5: Hobbit(3, "Peregrin", 28, "Side Kick"),
3737
}
3838

39-
session: Session = Session()
39+
session: Session = await Session.create()
4040
try:
4141
namedMap: NamedMap[int, Hobbit] = await session.get_map("aggregation-test")
4242

‎examples/basics.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ async def do_run() -> None:
1414
1515
:return: None
1616
"""
17-
session: Session = Session()
17+
session: Session = await Session.create()
1818
try:
1919
namedMap: NamedMap[int, str] = await session.get_map("my-map")
2020

‎examples/events.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ async def do_run() -> None:
1515
1616
:return: None
1717
"""
18-
session: Session = Session()
18+
session: Session = await Session.create()
1919
try:
2020
namedMap: NamedMap[int, str] = await session.get_map("listeners-map")
2121
await namedMap.put(1, "1")

‎examples/filters.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ async def do_run() -> None:
2828
2929
:return: None
3030
"""
31-
session: Session = Session()
31+
session: Session = await Session.create()
3232
try:
3333
homes: List[str] = ["Hobbiton", "Buckland", "Frogmorton", "Stock"]
3434
namedMap: NamedMap[int, Hobbit] = await session.get_map("hobbits")

‎examples/processors.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ async def do_run() -> None:
2626
2727
:return: None
2828
"""
29-
session: Session = Session()
29+
session: Session = await Session.create()
3030
try:
3131
namedMap: NamedMap[int, Hobbit] = await session.get_map("hobbits")
3232

‎examples/python_object_keys_and_values.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ async def do_run() -> None:
2929
3030
:return: None
3131
"""
32-
session: Session = Session()
32+
session: Session = await Session.create()
3333
try:
3434
namedMap: NamedMap[AccountKey, Account] = await session.get_map("accounts")
3535

‎pyproject.toml

+11-11
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,24 @@ classifiers = [
2121
]
2222

2323
[tool.poetry.dependencies]
24-
python = "~3.10"
25-
protobuf = "~4.21"
26-
grpcio = "~1.51"
27-
grpcio-tools = "~1.51"
28-
jsonpickle = "~2.2"
24+
python = "~3.11"
25+
protobuf = "~4.23"
26+
grpcio = "~1.54"
27+
grpcio-tools = "~1.54"
28+
jsonpickle = "~3.0"
2929
pymitter = "~0.4"
3030

3131
[tool.poetry.dev-dependencies]
32-
pytest = "~7.2"
33-
pytest-asyncio = "~0.19"
34-
pytest-cov = "~4.0"
32+
pytest = "~7.3"
33+
pytest-asyncio = "~0.21"
34+
pytest-cov = "~4.1"
3535
pytest-unordered = "~0.5"
36-
pre-commit = "~2.20"
36+
pre-commit = "~3.3"
3737
Sphinx = "~4.5"
38-
sphinx-rtd-theme = "~1.1"
38+
sphinx-rtd-theme = "~1.2"
3939
sphinxcontrib-napoleon = "~0.7"
4040
m2r = "~0.3"
41-
third-party-license-file-generator = "~2022.3"
41+
third-party-license-file-generator = "~2023.2"
4242

4343
[tool.pytest.ini_options]
4444
pythonpath = ["src"]

‎src/coherence/__init__.py

+10
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
__version__ = "1.0b1"
88

9+
import logging
10+
911
# expose these symbols in top-level namespace
1012
from .aggregator import Aggregators as Aggregators
1113
from .client import MapEntry as MapEntry
@@ -16,3 +18,11 @@
1618
from .client import TlsOptions as TlsOptions
1719
from .filter import Filters as Filters
1820
from .processor import Processors as Processors
21+
22+
# default logging configuration for coherence
23+
handler: logging.StreamHandler = logging.StreamHandler() # type: ignore
24+
handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
25+
26+
COH_LOG = logging.getLogger("coherence")
27+
COH_LOG.setLevel(logging.INFO)
28+
COH_LOG.addHandler(handler)

‎src/coherence/client.py

+74-18
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66

77
import abc
88
import asyncio
9+
import logging
910
import os
10-
from asyncio import Task
11+
from asyncio import Condition, Task
1112
from threading import Lock
1213
from typing import (
1314
Any,
@@ -45,6 +46,8 @@
4546
R = TypeVar("R")
4647
T = TypeVar("T")
4748

49+
COH_LOG = logging.getLogger("coherence")
50+
4851

4952
@no_type_check
5053
def _pre_call_cache(func):
@@ -58,6 +61,9 @@ async def inner_async(self, *args, **kwargs):
5861
if not self.active:
5962
raise Exception("Cache [{}] has been {}.".format(self.name, "released" if self.released else "destroyed"))
6063

64+
# noinspection PyProtectedMember
65+
await self._session._wait_for_active()
66+
6167
return await func(self, *args, **kwargs)
6268

6369
if asyncio.iscoroutinefunction(func):
@@ -499,6 +505,7 @@ def __init__(self, cache_name: str, session: Session, serializer: Serializer):
499505
self._internal_emitter: EventEmitter = EventEmitter()
500506
self._destroyed: bool = False
501507
self._released: bool = False
508+
self._session: Session = session
502509
from .event import _MapEventsManager
503510

504511
self._setup_event_handlers()
@@ -743,16 +750,18 @@ def _setup_event_handlers(self) -> None:
743750
# noinspection PyProtectedMember
744751
def on_destroyed(name: str) -> None:
745752
if name == cache_name:
746-
this._events_manager._close()
747-
this._destroyed = True
748-
emitter.emit(MapLifecycleEvent.DESTROYED.value, name)
753+
if not this.destroyed:
754+
this._events_manager._close()
755+
this._destroyed = True
756+
emitter.emit(MapLifecycleEvent.DESTROYED.value, name)
749757

750758
# noinspection PyProtectedMember
751759
def on_released(name: str) -> None:
752760
if name == cache_name:
753-
this._events_manager._close()
754-
this._released = True
755-
emitter.emit(MapLifecycleEvent.RELEASED.value, name)
761+
if not this.released:
762+
this._events_manager._close()
763+
this._released = True
764+
emitter.emit(MapLifecycleEvent.RELEASED.value, name)
756765

757766
def on_truncated(name: str) -> None:
758767
if name == cache_name:
@@ -922,7 +931,7 @@ def __init__(
922931
corresponding `ConfigurableCacheFactory` on the server.
923932
:param request_timeout_seconds: Defines the request timeout, in `seconds`, that will be applied to each
924933
remote call. If not explicitly set, this defaults to :func:`coherence.client.Options.DEFAULT_REQUEST_TIMEOUT`.
925-
See also See also :func:`coherence.client.Options.ENV_REQUEST_TIMEOUT`
934+
See also :func:`coherence.client.Options.ENV_REQUEST_TIMEOUT`
926935
:param ser_format: The serialization format. Currently, this is always `json`
927936
:param channel_options: The `gRPC` `ChannelOptions`. See
928937
https://grpc.github.io/grpc/python/glossary.html#term-channel_arguments and
@@ -941,7 +950,8 @@ def __init__(
941950
try:
942951
time_out = float(timeout)
943952
except ValueError:
944-
print(f"The value of {Options.ENV_REQUEST_TIMEOUT} cannot be converted to a float")
953+
COH_LOG.warning("The timeout value of [%s] cannot be converted to a float", Options.ENV_REQUEST_TIMEOUT)
954+
945955
self._request_timeout_seconds = time_out
946956
else:
947957
self._request_timeout_seconds = request_timeout_seconds
@@ -1080,6 +1090,8 @@ def __init__(self, session_options: Optional[Options] = None):
10801090
:param session_options: the provided :func:`coherence.client.Options`
10811091
"""
10821092
self._closed: bool = False
1093+
self._active = False
1094+
self._active_condition: Condition = Condition()
10831095
self._caches: dict[str, NamedCache[Any, Any]] = dict()
10841096
self._lock: Lock = Lock()
10851097
if session_options is not None:
@@ -1121,6 +1133,13 @@ def __init__(self, session_options: Optional[Options] = None):
11211133
watch_task: Task[None] = asyncio.create_task(watch_channel_state(self))
11221134
self._tasks.add(watch_task)
11231135
self._emitter: EventEmitter = EventEmitter()
1136+
self._channel.get_state(True) # trigger connect
1137+
1138+
@staticmethod
1139+
async def create(session_options: Optional[Options] = None) -> Session:
1140+
session: Session = Session(session_options)
1141+
await session._set_active(False)
1142+
return session
11241143

11251144
# noinspection PyTypeHints
11261145
@_pre_call_session
@@ -1236,6 +1255,30 @@ async def get_map(self, name: str, ser_format: str = DEFAULT_FORMAT) -> "NamedMa
12361255
self._caches.update({name: c})
12371256
return c
12381257

1258+
def is_active(self) -> bool:
1259+
"""
1260+
Returns
1261+
:return:
1262+
"""
1263+
return self._active
1264+
1265+
async def _set_active(self, active: bool) -> None:
1266+
self._active = active
1267+
if self._active:
1268+
if not self._active_condition.locked():
1269+
await self._active_condition.acquire()
1270+
self._active_condition.notify_all()
1271+
self._active_condition.release()
1272+
else:
1273+
await self._active_condition.acquire()
1274+
1275+
async def _wait_for_active(self) -> None:
1276+
if not self.is_active():
1277+
timeout: float = self._session_options.request_timeout_seconds
1278+
COH_LOG.debug("Waiting for session to become active; timeout=[%s seconds]", timeout)
1279+
async with asyncio.timeout(timeout):
1280+
await self._active_condition.wait()
1281+
12391282
# noinspection PyUnresolvedReferences
12401283
async def close(self) -> None:
12411284
"""
@@ -1248,6 +1291,10 @@ async def close(self) -> None:
12481291
task.cancel()
12491292
self._tasks.clear()
12501293

1294+
caches_copy: dict[str, NamedCache[Any, Any]] = self._caches.copy()
1295+
for cache in caches_copy.values():
1296+
await cache.destroy()
1297+
12511298
await self._channel.close() # TODO: consider grace period?
12521299

12531300
def _setup_event_handlers(self, client: NamedCacheClient[K, V]) -> None:
@@ -1342,28 +1389,37 @@ async def watch_channel_state(session: Session) -> None:
13421389
emitter: EventEmitter = session._emitter
13431390
channel: grpc.aio.Channel = session.channel
13441391
first_connect: bool = True
1345-
connected: bool = False
1392+
last_state: grpc.ChannelConnectivity = grpc.ChannelConnectivity.IDLE
13461393

13471394
try:
13481395
while True:
13491396
state: grpc.ChannelConnectivity = channel.get_state(False)
1397+
COH_LOG.debug("New Channel State: transitioning from [%s] to [%s]", last_state, state)
13501398
match state:
13511399
case grpc.ChannelConnectivity.SHUTDOWN:
1352-
continue # nothing to do
1400+
COH_LOG.info("Session to [%s] terminated", session.options.address)
1401+
await session._set_active(False)
13531402
case grpc.ChannelConnectivity.READY:
1354-
if not first_connect and not connected:
1403+
if not first_connect and not session.is_active():
1404+
COH_LOG.info("Session re-established to [%s]", session.options.address)
1405+
13551406
await emitter.emit_async(SessionLifecycleEvent.RECONNECTED.value)
1356-
connected = True
1357-
elif first_connect and not connected:
1407+
await session._set_active(True)
1408+
elif first_connect and not session.is_active():
1409+
COH_LOG.info("Session established to [%s]", session.options.address)
1410+
13581411
first_connect = False
1359-
connected = True
13601412
await emitter.emit_async(SessionLifecycleEvent.CONNECTED.value)
1413+
await session._set_active(True)
13611414
case _:
1362-
if connected:
1415+
if session.is_active():
1416+
COH_LOG.warning("Session to [%s] disconnected; will attempt reconnect", session.options.address)
1417+
13631418
await emitter.emit_async(SessionLifecycleEvent.DISCONNECTED.value)
1364-
connected = False
1419+
await session._set_active(False)
13651420

1366-
await channel.wait_for_state_change(state)
1421+
COH_LOG.debug("Waiting for state change from [%s]", state)
1422+
await channel.wait_for_state_change(channel.get_state(True))
13671423
except asyncio.CancelledError:
13681424
return
13691425

0 commit comments

Comments
 (0)
Please sign in to comment.