> On Jan 22, 2025, at 1:19 PM, k...@krot.org wrote:
> 
> 
> • Glyph [2025-01-22 18:25]:
>> In general, if you have to call reactor.stop() manually like this in a 
>> script, you're probably doing too much manual fiddling with its lifecycle.  
>> I have not fully debugged this example, but I suspect it's just because the 
>> error is synchronous before the reactor starts.
> 
> The provided scripts are just minimal examples which illustrate the problem I 
> am having in a bigger piece of a daemon, which sets up a number of listeners 
> on multiple interfaces and ports: if it is not able to bind to any of those, 
> reactor should stop. It it does not stop by calling reactor.stop(), only by 
> wrapping it in callWhenRunning or callLater. At first I just thought that 
> it's just the way it is, but after I started the question WHY, it bothers me 
> a little bit.

Are these listeners set up later than startup time, though?  If you do 
something like:

async def main():
    try:
        await myEndpoint.listen(...)
    except CannotListenError:
        print("can't listen, goodbye")
        return
    serveForever = Deferred()
    await serveForever

you will achieve that early-exit you want.

>> However, rather than carefully managing the reactor's startup state like 
>> this where half your code runs synchronously before reactor startup,
> 
> There is no synchronous code in the examples though, apart from setting up 
> some things which would be called once reactor is running.

I guess "synchronous" is a slippery word here, "eager" might be a better one, 
but the salient difference is that TCP4ServerEndpoint.listen is going to call 
socket.socket.listen() immediately when you invoke it, and then it is going to 
raise an error and fire its Deferred immediately, which will result in running 
those callbacks early.

> E.g. the first two defs set up callback and erroback functions -- isn't 
> reactor.running supposed to be True when callbacks/errorbacks attached to a 
> deferred are called?

No, the reactor has nothing to do with Deferred.  Now that we have managed to 
deprecate and remove the ill-conceived "Deferred.setTimeout" there is no 
interaction between the two.

Consider this example, with no reactor anywhere:

>>> >>> from twisted.internet.defer import Deferred
>>> >>> d = Deferred()
>>> >>> async def awaiter():
>>> ...     print("hello")
>>> ...     value = await d
>>> ...     print(f"goodbye: {value}")
>>> ...     
>>> ... 
>>> >>> d2 = Deferred.fromCoroutine(awaiter())
>>> hello
>>> >>> 3 + 4
>>> 7
>>> >>> d.callback(_)
>>> goodbye: 7



reactor.running means something extremely subtle about the run-state of the 
reactor, which is only loosely related to whether you will get a 
ReactorNotRunning error calling stop() or not; the reactor continues running 
for a while after stop is called, during which period .running is True, but 
.stop() will raise an exception.  I realize the naming of the exception here is 
not the best.

The rule is "don't call reactor.stop() more than once", not "don't call 
reactor.stop() if reactor.running is True".

> as an experiment, here's example which uses react():
> 
> #!/usr/local/bin/python3
> 
> from twisted.internet import reactor, defer, protocol
> from twisted.internet.protocol import Factory
> from twisted.internet.endpoints import TCP4ServerEndpoint
> from twisted.internet.task import react
> 
> def _port(port):
>    print('got', port)
> 
> def _error(err):
>    print('got err', err)
>    print('is reactor running in error?', reactor.running)
>    print('is reactor running in lambda?', (lambda: reactor.running)())
>    reactor.stop() # <- this fails
> #    reactor.callWhenRunning(reactor.stop) # <- this works
> 
> async def main(reactor):
>    print('running in main, huh?', reactor.running)
>    d = TCP4ServerEndpoint(reactor, 123).listen(Factory())
>    d.addCallback(_port)
>    d.addErrback(_error)
>    d.addErrback(print)

What is happening here is that you get called, the reactor starts, then you 
immediately synchronously return without awaiting anything, shutting down the 
reactor.  If you're using react(), it's react()'s job to call reactor.stop; all 
you can safely do is fire the Deferred (or complete the coroutine) returned 
from main().

> react(main)
> 
> it outputs this:
> 
> running in main, huh? False
> got err [Failure instance: Traceback: <class 
> 'twisted.internet.error.CannotListenError'>: Couldn't listen on any:123: 
> [Errno 13] Permission denied.
> /usr/local/lib/python3.12/site-packages/twisted/internet/defer.py:155:execute
> /usr/local/lib/python3.12/site-packages/twisted/internet/posixbase.py:366:listenTCP
> /usr/local/lib/python3.12/site-packages/twisted/internet/tcp.py:1360:startListening
> ]
> is reactor running in error? False
> is reactor running in lambda? False
> [Failure instance: Traceback: <class 
> 'twisted.internet.error.ReactorNotRunning'>: Can't stop reactor that isn't 
> running.
> /usr/local/lib/python3.12/site-packages/twisted/internet/defer.py:1088:_runCallbacks
> /home/km/./foo:15:_error
> /usr/local/lib/python3.12/site-packages/twisted/internet/base.py:790:stop
> ]

Yep, this code stops the reactor twice: once in the code that concludes react() 
and once in _error.  If you manage to fully exit the reactor before that second 
call to reactor.stop() happens (which is what I think your callWhenRunning 
does), then you won't see an exception.  But that is an accident of scheduling, 
so I would not rely on it for correctness here.

-g


_______________________________________________
Twisted mailing list -- twisted@python.org
To unsubscribe send an email to twisted-le...@python.org
https://mail.python.org/mailman3/lists/twisted.python.org/
Message archived at 
https://mail.python.org/archives/list/twisted@python.org/message/CSD5IXMJDDAZVA35IBBQHH7VY72SRQUR/
Code of Conduct: https://twisted.org/conduct

Reply via email to