-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Johny b/561 non context manager #600
Conversation
Meeting summary:
|
@@ -59,6 +59,24 @@ class Golem(_Engine): | |||
at any given time. | |||
""" | |||
|
|||
async def start(self) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
weren't these methods supposed to be implemented on the _Engine
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- All logic is on
_Engine
. - Those are public methods.
--> I think this is 100% in line with the Twist discussion
when the engine is used for the first time. | ||
""" | ||
if not self.operative: | ||
await self._start() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've noticed that there's a difference between:
async with Golem(...) as golem:
do_stuff_with(golem)
and
golem = Golem(...)
await golem.start()
do_stuff_with(golem)
await golem.stop()
in case an exception is raised in golem._start()
.
With context manager, golem._stop()
will be called before re-raising the exception.
With the start()/stop()
combo, the developer has to make sure that stop()
is called when start()
raises an exception. That's fine, if it's intentional and we document this clearly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that if the developer has any code that ensures golem
is stopped when an exception is raised in do_stuff_with(golem)
it will also work for start
?
E.g. our "recommended" way of doing things without context manager would be
golem = Golem(...)
try:
do_stuff_with(golem)
finally:
golem.stop()
(I skipped 'start` because it is optional and I don't think we need it in our examples)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@johny-b As I said, that's fine for me. But let's remember about this when creating documentation for start()/stop()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'll write the docs the way asyncio.Lock is documented, so
"the preferred way is"
"which is equivalent to"
yapapi/engine.py
Outdated
|
||
stack.push(report_shutdown) | ||
async def _start(self) -> None: | ||
self._operative = True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't we set this to True
at the end of the method? If any error occurs before the end of initialization we may end up with a Golem
instance that is operative
but not properly initialized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My reasoning was that we have to guard against more than one start
running at the same time, as in:
golem = Golem(...)
loop.create_task(golem.start())
loop.create_task(golem.start())
(I don't think anyone will write exactly this but this might somehow happen in a more complex code, I guess?).
We could store two bits of information, start_started
, start_ended
.
But maybe we should renameoperative
? It is now if fact started_not_stopped
. Or maybe just remove a public property?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@johny-b You've raised a good point. Consider again your example:
golem = Golem(...)
loop.create_task(golem.start()) # 1
loop.create_task(golem.start()) # 2
Suppose golem.start()
in line #1
is in progress and golem.start()
in #2
starts. We don't want the second instance of golem._start()
to run concurrently with the first one, and your current solution will prevent that. But we also don't want golem.start()
in line #2
exiting immediately, as if golem
were already properly started, while _start()
is still in progress! This is even more important when we replace both golem.start()
's with golem.execute_tasks()
.
What we do want is for the second golem.start()
to wait until the first one finishes. So we need some kind of task synchronisation mechanism, e.g. an asyncio.Lock
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a really good point. I need to think about this : )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 0829514
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@johny-b Thanks. Now it probably works well, but the solution using asyncio.Futures
is a bit hard to follow, at least for me.
Why not something more straightforward, with asyncio.Lock
:
def __init__():
self._started = False
self._stopped = False
self._lock = asyncio.Lock()
async def start():
async with self._lock:
if not self._started:
await self._start()
self._started = True
# Similarly for stop()
@property
def operative():
return self._started and not self._stopped
EDIT: Perhaps stop()
should differ from start()
in that self._stopped = True
goes before await self.stop()
. And there's the complication with the exc_info
tuple being passed around from __aexit__()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additionally, if you put start()
and stop()
in _Engine
instead of Golem
, you could define _Engine
's __aenter__()
and __aexit__()
in terms of start()
and stop()
. That would also protect us from concurrent executions of async with golem:...
in two different tasks.
Changes: * all logic related to explicit calls to `start()` and `stop()` is implemented on the Golem object * calling multiple `start()` concurrently is harmless * restarting a Golem is explicitly forbidden (to be fixed in #606)
Now I know how to do this, an also after recent |
GENERAL NOTES
This resolves #561, but there are follow-up tasks:
start
again a Golem that wasstopped
#606THINGS DONE
_Engine._start()
,_Engine._stop()
methods (no logic changes, just extracted code from__aenter__/__aexit__
)Golem.operative
propertyGolem.start()
/Golem.stop()
start()
is called inGolem.execute_tasks
andGolem.run_service
if it wasn't already called before