Skip to content

Commit 2a9dcd7

Browse files
authored
Revert "enhancement: Add Lifespan Events Handling In ASGI app (#113)" (#119)
This reverts commit 2aa17da.
1 parent 2aa17da commit 2a9dcd7

File tree

10 files changed

+30
-614
lines changed

10 files changed

+30
-614
lines changed

.github/workflows/unit-test.yaml

Lines changed: 0 additions & 37 deletions
This file was deleted.

examples/example.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,24 @@
1212
# pylint: disable=C0116
1313
# pylint: disable=W0613
1414

15-
import asyncio
1615
import restate
1716

18-
from concurrent_greeter import concurrent_greeter
1917
from greeter import greeter
20-
from pydantic_greeter import pydantic_greeter
2118
from virtual_object import counter
2219
from workflow import payment
23-
24-
20+
from pydantic_greeter import pydantic_greeter
21+
from concurrent_greeter import concurrent_greeter
2522

2623
app = restate.app(services=[greeter,
2724
counter,
2825
payment,
2926
pydantic_greeter,
30-
concurrent_greeter,
31-
],)
27+
concurrent_greeter])
3228

3329
if __name__ == "__main__":
3430
import hypercorn
3531
import hypercorn.asyncio
32+
import asyncio
3633
conf = hypercorn.Config()
3734
conf.bind = ["0.0.0.0:9080"]
3835
asyncio.run(hypercorn.asyncio.serve(app, conf))

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Source = "https://github.com/restatedev/sdk-python"
2121
"Bug Tracker" = "https://github.com/restatedev/sdk-python/issues"
2222

2323
[project.optional-dependencies]
24-
test = ["pytest", "hypercorn", "pytest-asyncio==1.1.0"]
24+
test = ["pytest", "hypercorn"]
2525
lint = ["mypy", "pylint"]
2626
harness = ["testcontainers", "hypercorn", "httpx"]
2727
serde = ["dacite", "pydantic"]

python/restate/aws_lambda.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
Receive,
2222
HTTPResponseStartEvent,
2323
HTTPResponseBodyEvent,
24-
HTTPRequestEvent, Send)
24+
HTTPRequestEvent)
2525

2626
class RestateLambdaRequest(TypedDict):
2727
"""
@@ -162,9 +162,8 @@ def lambda_handler(event: RestateLambdaRequest, _context: Any) -> RestateLambdaR
162162
scope = create_scope(event)
163163
recv = request_to_receive(event)
164164
send = ResponseCollector()
165-
send_typed = cast(Send, send)
166165

167-
asgi_instance = asgi_app(scope, recv, send_typed)
166+
asgi_instance = asgi_app(scope, recv, send)
168167
asgi_task = loop.create_task(asgi_instance) # type: ignore[var-annotated, arg-type]
169168
loop.run_until_complete(asgi_task)
170169

python/restate/context.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,8 @@ def request(self) -> Request:
220220
"""
221221

222222

223-
@overload
224223
@typing_extensions.deprecated("`run` is deprecated, use `run_typed` instead for better type safety")
224+
@overload
225225
@abc.abstractmethod
226226
def run(self,
227227
name: str,
@@ -250,8 +250,8 @@ def run(self,
250250
251251
"""
252252

253-
@overload
254253
@typing_extensions.deprecated("`run` is deprecated, use `run_typed` instead for better type safety")
254+
@overload
255255
@abc.abstractmethod
256256
def run(self,
257257
name: str,
@@ -280,7 +280,6 @@ def run(self,
280280
281281
"""
282282

283-
@overload
284283
@typing_extensions.deprecated("`run` is deprecated, use `run_typed` instead for better type safety")
285284
@abc.abstractmethod
286285
def run(self,

python/restate/endpoint.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
import typing
1717

18-
from restate.server_types import LifeSpan
1918
from restate.service import Service
2019
from restate.object import VirtualObject
2120
from restate.workflow import Workflow
@@ -81,7 +80,7 @@ def identity_key(self, identity_key: str):
8180
"""Add an identity key to this endpoint."""
8281
self.identity_keys.append(identity_key)
8382

84-
def app(self, lifespan: typing.Optional[LifeSpan] = None):
83+
def app(self):
8584
"""
8685
Returns the ASGI application for this endpoint.
8786
@@ -95,13 +94,12 @@ def app(self, lifespan: typing.Optional[LifeSpan] = None):
9594
# pylint: disable=C0415
9695
# pylint: disable=R0401
9796
from restate.server import asgi_app
98-
return asgi_app(self, lifespan)
97+
return asgi_app(self)
9998

10099
def app(
101100
services: typing.Iterable[typing.Union[Service, VirtualObject, Workflow]],
102101
protocol: typing.Optional[typing.Literal["bidi", "request_response"]] = None,
103-
identity_keys: typing.Optional[typing.List[str]] = None,
104-
lifespan: typing.Optional[LifeSpan] = None):
102+
identity_keys: typing.Optional[typing.List[str]] = None):
105103
"""A restate ASGI application that hosts the given services."""
106104
endpoint = Endpoint()
107105
if protocol == "bidi":
@@ -113,4 +111,4 @@ def app(
113111
if identity_keys:
114112
for key in identity_keys:
115113
endpoint.identity_key(key)
116-
return endpoint.app(lifespan)
114+
return endpoint.app()

python/restate/server.py

Lines changed: 12 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,12 @@
1111
"""This module contains the ASGI server for the restate framework."""
1212

1313
import asyncio
14-
import inspect
15-
from typing import Any, Dict, TypedDict, Literal
14+
from typing import Dict, TypedDict, Literal
1615
import traceback
17-
import typing
1816
from restate.discovery import compute_discovery_json
1917
from restate.endpoint import Endpoint
2018
from restate.server_context import ServerInvocationContext, DisconnectedException
21-
from restate.server_types import Receive, ReceiveChannel, Scope, Send, binary_to_header, header_to_binary, LifeSpan # pylint: disable=line-too-long
19+
from restate.server_types import Receive, ReceiveChannel, Scope, Send, binary_to_header, header_to_binary # pylint: disable=line-too-long
2220
from restate.vm import VMWrapper
2321
from restate._internal import PyIdentityVerifier, IdentityVerificationException # pylint: disable=import-error,no-name-in-module
2422
from restate._internal import SDK_VERSION # pylint: disable=import-error,no-name-in-module
@@ -188,6 +186,10 @@ async def process_invocation_to_completion(vm: VMWrapper,
188186
finally:
189187
context.on_attempt_finished()
190188

189+
class LifeSpanNotImplemented(ValueError):
190+
"""Signal to the asgi server that we didn't implement lifespans"""
191+
192+
191193
class ParsedPath(TypedDict):
192194
"""Parsed path from the request."""
193195
type: Literal["invocation", "health", "discover", "unknown"]
@@ -214,55 +216,8 @@ def parse_path(request: str) -> ParsedPath:
214216
# anything other than invoke is 404
215217
return { "type": "unknown" , "service": None, "handler": None }
216218

217-
def is_async_context_manager(obj: Any):
218-
"""check if passed object is an async context manager"""
219-
return (hasattr(obj, '__aenter__') and
220-
hasattr(obj, '__aexit__') and
221-
inspect.iscoroutinefunction(obj.__aenter__) and
222-
inspect.iscoroutinefunction(obj.__aexit__))
223219

224-
225-
async def lifespan_processor(
226-
scope: Scope,
227-
receive: Receive,
228-
send: Send,
229-
lifespan: LifeSpan
230-
) -> None:
231-
"""Process lifespan context manager."""
232-
started = False
233-
assert scope["type"] in ["lifespan", "lifespan.startup", "lifespan.shutdown"]
234-
assert is_async_context_manager(lifespan()), "lifespan must be an async context manager"
235-
await receive()
236-
try:
237-
async with lifespan() as maybe_state:
238-
if maybe_state is not None:
239-
if "state" not in scope:
240-
raise RuntimeError("The server does not support state in lifespan")
241-
scope["state"] = maybe_state
242-
await send({
243-
"type": "lifespan.startup.complete", # type: ignore
244-
})
245-
started = True
246-
await receive()
247-
except Exception:
248-
exc_text = traceback.format_exc()
249-
if started:
250-
await send({
251-
"type": "lifespan.shutdown.failed",
252-
"message": exc_text
253-
})
254-
else:
255-
await send({
256-
"type": "lifespan.startup.failed",
257-
"message": exc_text
258-
})
259-
raise
260-
await send({
261-
"type": "lifespan.shutdown.complete" # type: ignore
262-
})
263-
264-
# pylint: disable=too-many-return-statements
265-
def asgi_app(endpoint: Endpoint, lifespan: typing.Optional[LifeSpan] = None):
220+
def asgi_app(endpoint: Endpoint):
266221
"""Create an ASGI-3 app for the given endpoint."""
267222

268223
# Prepare request signer
@@ -271,17 +226,14 @@ def asgi_app(endpoint: Endpoint, lifespan: typing.Optional[LifeSpan] = None):
271226
async def app(scope: Scope, receive: Receive, send: Send):
272227
try:
273228
if scope['type'] == 'lifespan':
274-
if lifespan is not None:
275-
await lifespan_processor(scope, receive, send, lifespan)
276-
return
277-
return
278-
229+
raise LifeSpanNotImplemented()
279230
if scope['type'] != 'http':
280231
raise NotImplementedError(f"Unknown scope type {scope['type']}")
281232

282233
request_path = scope['path']
283234
assert isinstance(request_path, str)
284235
request: ParsedPath = parse_path(request_path)
236+
285237
# Health check
286238
if request['type'] == 'health':
287239
await send_health_check(send)
@@ -297,6 +249,7 @@ async def app(scope: Scope, receive: Receive, send: Send):
297249
# Identify verification failed, send back unauthorized and close
298250
await send_status(send, receive, 401)
299251
return
252+
300253
# might be a discovery request
301254
if request['type'] == 'discover':
302255
await send_discovery(scope, send, endpoint)
@@ -330,6 +283,8 @@ async def app(scope: Scope, receive: Receive, send: Send):
330283
send)
331284
finally:
332285
await receive_channel.close()
286+
except LifeSpanNotImplemented as e:
287+
raise e
333288
except Exception as e:
334289
traceback.print_exc()
335290
raise e

python/restate/server_types.py

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616

1717
import asyncio
1818
from typing import (Awaitable, Callable, Dict, Iterable, List,
19-
Tuple, Union, TypedDict, Literal, Optional,
20-
NotRequired, Any, AsyncContextManager)
19+
Tuple, Union, TypedDict, Literal, Optional, NotRequired, Any)
2120

2221
class ASGIVersions(TypedDict):
2322
"""ASGI Versions"""
@@ -26,7 +25,7 @@ class ASGIVersions(TypedDict):
2625

2726
class Scope(TypedDict):
2827
"""ASGI Scope"""
29-
type: Literal["http", "lifespan", "lifespan.startup", "lifespan.shutdown"]
28+
type: Literal["http"]
3029
asgi: ASGIVersions
3130
http_version: str
3231
method: str
@@ -65,29 +64,18 @@ class HTTPResponseBodyEvent(TypedDict):
6564
body: bytes
6665
more_body: bool
6766

68-
class LifeSpanEvent(TypedDict):
69-
"""ASGI LifeSpan event"""
70-
type: Literal["lifespan.startup.complete",
71-
"lifespan.shutdown.complete",
72-
"lifespan.startup.failed",
73-
"lifespan.shutdown.failed"]
74-
message: Optional[str]
7567

76-
77-
ASGIReceiveEvent = Union[HTTPRequestEvent, LifeSpanEvent]
68+
ASGIReceiveEvent = HTTPRequestEvent
7869

7970

8071
ASGISendEvent = Union[
8172
HTTPResponseStartEvent,
82-
HTTPResponseBodyEvent,
83-
LifeSpanEvent
73+
HTTPResponseBodyEvent
8474
]
8575

8676
Receive = Callable[[], Awaitable[ASGIReceiveEvent]]
8777
Send = Callable[[ASGISendEvent], Awaitable[None]]
8878

89-
LifeSpan = Callable[[], AsyncContextManager[Any]]
90-
9179
ASGIApp = Callable[
9280
[
9381
Scope,
@@ -96,6 +84,7 @@ class LifeSpanEvent(TypedDict):
9684
],
9785
Awaitable[None],
9886
]
87+
9988
def header_to_binary(headers: Iterable[Tuple[str, str]]) -> List[Tuple[bytes, bytes]]:
10089
"""Convert a list of headers to a list of binary headers."""
10190
return [ (k.encode('utf-8'), v.encode('utf-8')) for k,v in headers ]

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,3 @@ pydantic
77
httpx
88
testcontainers
99
typing-extensions>=4.14.0
10-
pytest-asyncio==1.1.0

0 commit comments

Comments
 (0)