Skip to content

Commit 3d8ed5f

Browse files
authored
fix: improve error when exiting before running Nitric.run() (#145)
1 parent 43cd4e1 commit 3d8ed5f

File tree

16 files changed

+102
-74
lines changed

16 files changed

+102
-74
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,5 @@ cython_debug/
149149
.vscode/
150150

151151
contracts/
152+
153+
testproj/

nitric/application.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
class Nitric:
2929
"""Represents a nitric app."""
3030

31+
_has_run = False
32+
3133
_workers: List[FunctionServer] = []
3234
_cache: Dict[str, Dict[str, Any]] = {
3335
"api": {},
@@ -61,13 +63,26 @@ def _create_resource(cls, resource: Type[BT], name: str, *args: Any, **kwargs: A
6163
"The nitric server may not be running or the host/port is inaccessible"
6264
) from cre
6365

66+
@classmethod
67+
def has_run(cls) -> bool:
68+
"""
69+
Check if the Nitric application has been started.
70+
71+
Returns:
72+
bool: True if the Nitric application has been started, False otherwise.
73+
"""
74+
return cls._has_run
75+
6476
@classmethod
6577
def run(cls) -> None:
6678
"""
6779
Start the nitric application.
6880
6981
This will execute in an existing event loop if there is one, otherwise it will attempt to create its own.
7082
"""
83+
if cls._has_run:
84+
print("The Nitric application has already been started, Nitric.run() should only be called once.")
85+
cls._has_run = True
7186
try:
7287
try:
7388
loop = asyncio.get_running_loop()

nitric/channel.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import atexit
2+
import re
3+
from urllib.parse import urlparse
4+
from grpclib.client import Channel
5+
6+
from nitric.application import Nitric
7+
from nitric.config import settings
8+
from nitric.exception import NitricNotRunningException
9+
10+
11+
def format_url(url: str):
12+
"""Add the default http scheme prefix to urls without one."""
13+
if not re.match("^((?:http|ftp|https):)?//", url.lower()):
14+
return "http://{0}".format(url)
15+
return url
16+
17+
18+
class ChannelManager:
19+
"""A singleton class to manage the gRPC channel."""
20+
21+
channel = None
22+
23+
@classmethod
24+
def get_channel(cls) -> Channel:
25+
"""Return the channel instance."""
26+
27+
if cls.channel is None:
28+
cls._create_channel()
29+
return cls.channel # type: ignore
30+
31+
@classmethod
32+
def _create_channel(cls):
33+
"""Create a new channel instance."""
34+
35+
channel_url = urlparse(format_url(settings.SERVICE_ADDRESS))
36+
cls.channel = Channel(host=channel_url.hostname, port=channel_url.port)
37+
atexit.register(cls._close_channel)
38+
39+
@classmethod
40+
def _close_channel(cls):
41+
"""Close the channel instance."""
42+
43+
if cls.channel is not None:
44+
cls.channel.close()
45+
cls.channel = None
46+
47+
# If the program exits without calling Nitric.run(), it may have been a mistake.
48+
if not Nitric.has_run():
49+
print(
50+
"WARNING: The Nitric application was not started. "
51+
"If you intended to start the application, call Nitric.run() before exiting."
52+
)

nitric/exception.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,13 @@ def __init__(self, message: str):
157157
super().__init__("Unable to connect to nitric server." + (" " + message if message else ""))
158158

159159

160+
class NitricNotRunningException(Exception):
161+
"""The Nitric application wasn't started before the program exited."""
162+
163+
def __init__(self):
164+
super().__init__("The Nitric application was not started, call Nitric.run() to start the application.")
165+
166+
160167
def exception_from_grpc_error(error: GRPCError):
161168
"""Translate a gRPC error to a nitric api exception."""
162169
return exception_from_grpc_code(error.status.value, error.message or "")

nitric/resources/apis.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
ResourceType,
6161
)
6262
from nitric.resources.resource import Resource as BaseResource
63-
from nitric.utils import new_default_channel
63+
from nitric.channel import ChannelManager
6464

6565

6666
@dataclass
@@ -479,7 +479,7 @@ async def _route_request_iterator(self):
479479

480480
async def start(self) -> None:
481481
"""Register this API route handler and handle http requests."""
482-
channel = new_default_channel()
482+
channel = ChannelManager.get_channel()
483483
server = ApiStub(channel=channel)
484484

485485
# Attach security rules for this route

nitric/resources/buckets.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
StorageWriteRequest,
5353
)
5454
from nitric.resources.resource import SecureResource
55-
from nitric.utils import new_default_channel
55+
from nitric.channel import ChannelManager
5656

5757

5858
class BucketNotifyRequest:
@@ -142,7 +142,7 @@ class BucketRef(object):
142142

143143
def __init__(self, name: str):
144144
"""Construct a Nitric Storage Client."""
145-
self._channel: Union[Channel, None] = new_default_channel()
145+
self._channel: Union[Channel, None] = ChannelManager.get_channel()
146146
self._storage_stub = StorageStub(channel=self._channel)
147147
self.name = name
148148

@@ -428,7 +428,7 @@ async def _listener_request_iterator(self):
428428

429429
async def start(self) -> None:
430430
"""Register this bucket listener and listen for events."""
431-
channel = new_default_channel()
431+
channel = ChannelManager.get_channel()
432432
server = StorageListenerStub(channel=channel)
433433

434434
try:

nitric/resources/kv.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@
4242
ResourceType,
4343
)
4444
from nitric.resources.resource import SecureResource
45-
from nitric.utils import dict_from_struct, new_default_channel, struct_from_dict
45+
from nitric.utils import dict_from_struct, struct_from_dict
46+
from nitric.channel import ChannelManager
4647

4748

4849
class KeyValueStoreRef:
@@ -54,7 +55,7 @@ class KeyValueStoreRef:
5455

5556
def __init__(self, name: str):
5657
"""Construct a reference to a deployed key value store."""
57-
self._channel: Channel = new_default_channel()
58+
self._channel: Channel = ChannelManager.get_channel()
5859
self._kv_stub = KvStoreStub(channel=self._channel)
5960
self.name = name
6061

nitric/resources/queues.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
from nitric.proto.queues.v1 import QueuesStub as QueueServiceStub
3232
from nitric.proto.resources.v1 import Action, ResourceDeclareRequest, ResourceIdentifier, ResourceType
3333
from nitric.resources.resource import SecureResource
34-
from nitric.utils import dict_from_struct, new_default_channel, struct_from_dict
34+
from nitric.utils import dict_from_struct, struct_from_dict
35+
from nitric.channel import ChannelManager
3536

3637

3738
@dataclass(frozen=True, order=True)
@@ -91,7 +92,7 @@ class QueueRef(object):
9192

9293
def __init__(self, name: str) -> None:
9394
"""Construct a Nitric Queue Client."""
94-
self._channel: Channel = new_default_channel()
95+
self._channel: Channel = ChannelManager.get_channel()
9596
self._queue_stub = QueueServiceStub(channel=self._channel)
9697
self.name = name
9798

nitric/resources/resource.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
ResourcesStub,
3535
ResourceType,
3636
)
37-
from nitric.utils import new_default_channel
37+
from nitric.channel import ChannelManager
3838

3939
T = TypeVar("T", bound="Resource")
4040

@@ -48,7 +48,7 @@ def __init__(self, name: str):
4848
"""Construct a new resource."""
4949
self.name = name
5050
self._reg: Optional[Task[Any]] = None # type: ignore
51-
self._channel = new_default_channel()
51+
self._channel = ChannelManager.get_channel()
5252
self._resources_stub = ResourcesStub(channel=self._channel)
5353

5454
@abstractmethod
@@ -85,9 +85,6 @@ def _perms_to_actions(self, *args: Any) -> List[Action]:
8585
pass
8686

8787
async def _register_policy_async(self, *args: str) -> None:
88-
# if self._reg is not None:
89-
# await asyncio.wait({self._reg})
90-
9188
policy = PolicyResource(
9289
principals=[ResourceIdentifier(type=ResourceType.Service)],
9390
actions=self._perms_to_actions(*args),

nitric/resources/schedules.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
ScheduleEvery,
3838
SchedulesStub,
3939
)
40-
from nitric.utils import new_default_channel
40+
from nitric.channel import ChannelManager
4141

4242

4343
class ScheduleServer(FunctionServer):
@@ -93,7 +93,7 @@ async def _schedule_request_iterator(self):
9393

9494
async def start(self) -> None:
9595
"""Register this schedule and start listening for requests."""
96-
channel = new_default_channel()
96+
channel = ChannelManager.get_channel()
9797
schedules_stub = SchedulesStub(channel=channel)
9898

9999
try:

0 commit comments

Comments
 (0)