Skip to content

Commit 04c868d

Browse files
arnavaghavmeta-codesync[bot]
authored andcommitted
Add fallback class implementation for cached_classproperty & helpers in cinderx
Summary: ### Change Remove `cached_classproperty` & other dummy class instantiation after swallowing import errors. ### Context The cinder stub implementation of `cached_classproperty` is a passive one which happens when import exception is swallowed in `__init__.py` of cinderx. This results in a false successful import & preventing `cinder`'s override logic from triggering here: https://fburl.com/code/7yf4nvnr Reviewed By: akatrevorjay Differential Revision: D86637741 fbshipit-source-id: 1dca04771c4ee3d346204908d7210336db427fd6
1 parent a4b7d2c commit 04c868d

1 file changed

Lines changed: 249 additions & 26 deletions

File tree

cinderx/PythonLib/cinderx/__init__.py

Lines changed: 249 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import gc
88
import platform
99
import sys
10-
1110
from os import environ
1211

1312
# ============================================================================
@@ -127,21 +126,19 @@ def _get_entire_call_stack_as_qualnames_with_lineno_and_frame() -> (
127126
def _is_compile_perf_trampoline_pre_fork_enabled() -> bool:
128127
return False
129128

130-
class async_cached_classproperty:
131-
pass
132-
133-
class async_cached_property:
134-
pass
135-
136-
class cached_classproperty:
137-
pass
138-
129+
from asyncio import AbstractEventLoop, Future
139130
from typing import (
131+
Awaitable,
140132
Callable,
133+
Dict,
141134
final,
135+
Generator,
142136
Generic,
137+
List,
138+
NoReturn,
143139
Optional,
144140
overload,
141+
Tuple,
145142
Type,
146143
TYPE_CHECKING,
147144
TypeVar,
@@ -150,23 +147,11 @@ class cached_classproperty:
150147
_TClass = TypeVar("_TClass")
151148
_TReturnType = TypeVar("_TReturnType")
152149

153-
if TYPE_CHECKING:
154-
from abc import ABC
155-
156-
@final
157-
class Descriptor(ABC, Generic[_TReturnType]):
158-
__name__: str
159-
__objclass__: Type[object]
160-
161-
def __get__(
162-
self, inst: object, ctx: Optional[Type[object]] = None
163-
) -> _TReturnType: ...
164-
165-
def __set__(self, inst: object, value: _TReturnType) -> None:
166-
pass
150+
@final
151+
class NoValueSet:
152+
pass
167153

168-
def __delete__(self, inst: object) -> None:
169-
pass
154+
NO_VALUE_SET = NoValueSet()
170155

171156
class _BaseCachedProperty(Generic[_TClass, _TReturnType]):
172157
fget: Callable[[_TClass], _TReturnType]
@@ -208,6 +193,244 @@ def __get__(
208193
obj.__dict__[self.__name__] = result
209194
return result
210195

196+
class _AsyncLazyValueState:
197+
NotStarted = 0
198+
Running = 1
199+
Done = 2
200+
201+
_T = TypeVar("_T", covariant=True)
202+
_TParams = TypeVar("_TParams")
203+
204+
# noqa: F401
205+
import asyncio
206+
207+
class _AsyncLazyValue(Awaitable[_T]):
208+
"""
209+
This is a low-level class used mainly for two things:
210+
* It helps to avoid calling a coroutine multiple times, by caching the
211+
result of a previous call
212+
* It ensures that the coroutine is called only once
213+
214+
_AsyncLazyValue has well defined cancellation behavior in these cases:
215+
216+
1. When we have a single task stack (call stack for you JS folks), which is
217+
awaiting on the AsyncLazyValue
218+
-> In this case, we mimic the behavior of a normal await. i.e: If the
219+
task stack gets cancelled, we cancel the coroutine (by raising a
220+
CancelledError in the underlying future)
221+
222+
2. When we have multiple task stacks awaiting on the future.
223+
We have two sub cases here.
224+
225+
2.1. The initial task stack (which resulted in an await of the coroutine)
226+
gets cancelled.
227+
-> In this case, we cancel the coroutine, and all the tasks depending
228+
on it. If we don't do that, we'd have to implement retry logic,
229+
which is a bad idea in such low level code. Even if we do implement
230+
retries, there's no guarantee that they would succeed, so it's better
231+
to just fail here.
232+
233+
Also, the number of times this happens is very small (I don't have
234+
data to prove it, but qualitative arguments suggest this is the
235+
case).
236+
237+
2.2. One of the many task stacks gets cancelled (but not the one which ended
238+
up awaiting the coroutine)
239+
-> In this case, we just allow the task stack to be cancelled, but
240+
the rest of them are processed without being affected.
241+
"""
242+
243+
def __init__(
244+
self,
245+
# pyre-fixme[31]: Expression `typing.Callable[(_TParams,
246+
# typing.Awaitable[_T])]` is not a valid type.
247+
coro_func: Callable[_TParams, Awaitable[_T]],
248+
# pyre-fixme[11]: Annotation `args` is not defined as a type.
249+
*args: _TParams.args,
250+
# pyre-fixme[11]: Annotation `kwargs` is not defined as a type.
251+
**kwargs: _TParams.kwargs,
252+
) -> None:
253+
global asyncio
254+
# pyre-fixme[31]: Expression `typing.Optional[typing.Callable[(_TParams,
255+
# typing.Awaitable[_T])]]` is not a valid type.
256+
self.coro_func: Optional[Callable[_TParams, Awaitable[_T]]] = coro_func
257+
self.args: Tuple[object, ...] = args
258+
self.kwargs: Dict[str, object] = kwargs
259+
self.state: int = _AsyncLazyValueState.NotStarted
260+
self.res: Optional[_T] = None
261+
self._futures: List[Future] = []
262+
self._awaiting_tasks = 0
263+
264+
async def _async_compute(self) -> _T:
265+
futures = self._futures
266+
try:
267+
coro_func = self.coro_func
268+
# lint-fixme: NoAssertsRule
269+
assert coro_func is not None
270+
self.res = res = await coro_func(*self.args, **self.kwargs)
271+
272+
self.state = _AsyncLazyValueState.Done
273+
274+
# pyre-fixme[1001]: Awaitable assigned to `value` is never awaited.
275+
for value in futures:
276+
if not value.done():
277+
value.set_result(self.res)
278+
279+
self.args = ()
280+
self.kwargs.clear()
281+
del self._futures[:]
282+
self.coro_func = None
283+
284+
return res
285+
286+
except (Exception, asyncio.CancelledError) as e:
287+
# pyre-fixme[1001]: Awaitable assigned to `value` is never awaited.
288+
for value in futures:
289+
if not value.done():
290+
value.set_exception(e)
291+
self._futures = []
292+
self.state = _AsyncLazyValueState.NotStarted
293+
raise
294+
295+
def _get_future(self, loop: Optional[AbstractEventLoop]) -> Future:
296+
if loop is None:
297+
loop = asyncio.get_event_loop()
298+
f = asyncio.Future(loop=loop)
299+
self._futures.append(f)
300+
self._awaiting_tasks += 1
301+
return f
302+
303+
def __iter__(self) -> _AsyncLazyValue[_T]:
304+
return self
305+
306+
def __next__(self) -> NoReturn:
307+
raise StopIteration(self.res)
308+
309+
def __await__(self) -> Generator[None, None, _T]:
310+
if self.state == _AsyncLazyValueState.Done:
311+
# pyre-ignore[7]: Expected `Generator[None, None, Variable[_T](covariant)]`
312+
# but got `_AsyncLazyValue[Variable[_T](covariant)]`.
313+
return self
314+
elif self.state == _AsyncLazyValueState.Running:
315+
c = self._get_future(None)
316+
return c.__await__()
317+
else:
318+
self.state = _AsyncLazyValueState.Running
319+
c = self._async_compute()
320+
return c.__await__()
321+
322+
def as_future(self, loop: AbstractEventLoop) -> Future:
323+
if self.state == _AsyncLazyValueState.Done:
324+
f = asyncio.Future(loop=loop)
325+
f.set_result(self.res)
326+
return f
327+
elif self.state == _AsyncLazyValueState.Running:
328+
return self._get_future(loop)
329+
else:
330+
if loop is None:
331+
loop = asyncio.get_event_loop()
332+
t = loop.create_task(self._async_compute())
333+
self.state = _AsyncLazyValueState.Running
334+
# pyre-ignore[16]: Undefined attribute `asyncio.tasks.Task`
335+
# has no attribute `_source_traceback`.
336+
if t._source_traceback:
337+
del t._source_traceback[-1]
338+
# pyre-fixme[7]: Expected `Future[Any]` but got `Task[_T]`.
339+
return t
340+
341+
_TAwaitableReturnType = TypeVar("_TAwaitableReturnType")
342+
343+
class async_cached_property(
344+
Generic[_TAwaitableReturnType, _TClass],
345+
_BaseCachedProperty[_TClass, Awaitable[_TAwaitableReturnType]],
346+
):
347+
def __init__(
348+
self,
349+
f: Callable[[_TClass], _TReturnType],
350+
slot: Optional[Descriptor[_TReturnType]] = None,
351+
) -> None:
352+
super().__init__(f, slot)
353+
354+
def __get__(
355+
self, obj: Optional[_TClass], cls: Type[_TClass]
356+
) -> (
357+
_BaseCachedProperty[_TClass, Awaitable[_TAwaitableReturnType]]
358+
| Awaitable[_TAwaitableReturnType]
359+
):
360+
if obj is None:
361+
return self
362+
363+
slot = self.slot
364+
if slot is not None:
365+
try:
366+
res = slot.__get__(obj, cls)
367+
except AttributeError:
368+
res = _AsyncLazyValue(self.fget, obj)
369+
slot.__set__(obj, res)
370+
return res
371+
372+
lazy_value = _AsyncLazyValue(self.fget, obj)
373+
setattr(obj, self.__name__, lazy_value)
374+
return lazy_value
375+
376+
class async_cached_classproperty(
377+
Generic[_TAwaitableReturnType, _TClass],
378+
_BaseCachedProperty[Type[_TClass], Awaitable[_TAwaitableReturnType]],
379+
):
380+
def __init__(
381+
self,
382+
f: Callable[[_TClass], Awaitable[_TAwaitableReturnType]],
383+
slot: Optional[Descriptor[Awaitable[_TAwaitableReturnType]]] = None,
384+
) -> None:
385+
super().__init__(f, slot)
386+
self._value: NoValueSet | Awaitable[_TAwaitableReturnType] = NO_VALUE_SET
387+
388+
def __get__(
389+
self, obj: Optional[_TClass], cls: Type[_TClass]
390+
) -> Awaitable[_TAwaitableReturnType]:
391+
lazy_value = self._value
392+
if not isinstance(lazy_value, NoValueSet):
393+
return lazy_value
394+
self._value = lazy_value = _AsyncLazyValue(self.fget, cls)
395+
return lazy_value
396+
397+
class cached_classproperty(_BaseCachedProperty[Type[_TClass], _TReturnType]):
398+
def __init__(
399+
self,
400+
f: Callable[[_TClass], _TReturnType],
401+
slot: Optional[Descriptor[_TReturnType]] = None,
402+
) -> None:
403+
super().__init__(f, slot)
404+
self._value: NoValueSet | _TReturnType = NO_VALUE_SET
405+
406+
def __get__(self, obj: Optional[_TClass], cls: Type[_TClass]) -> _TReturnType:
407+
result = self._value
408+
if not isinstance(result, NoValueSet):
409+
return result
410+
self._value = result = self.fget(cls)
411+
return result
412+
413+
_TClass = TypeVar("_TClass")
414+
_TReturnType = TypeVar("_TReturnType")
415+
416+
if TYPE_CHECKING:
417+
from abc import ABC
418+
419+
@final
420+
class Descriptor(ABC, Generic[_TReturnType]):
421+
__name__: str
422+
__objclass__: Type[object]
423+
424+
def __get__(
425+
self, inst: object, ctx: Optional[Type[object]] = None
426+
) -> _TReturnType: ...
427+
428+
def __set__(self, inst: object, value: _TReturnType) -> None:
429+
pass
430+
431+
def __delete__(self, inst: object) -> None:
432+
pass
433+
211434
class cached_property(_BaseCachedProperty[_TClass, _TReturnType]):
212435
def __init__(
213436
self,

0 commit comments

Comments
 (0)