asyncio: join sequence relies on callback chaining, so 'ready' fires before onJoin finishes and onJoin errors go unhandled

Abierto
#2,033 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
45/100
Tipo de issue
Error
Claridad
Bastante claro
Estado de actividad
Activo
Stack tecnológico
python

Línea de trabajo

Read src/autobahn/wamp/protocol.py:660-685 and the related join-sequence comments at lines 95 and 668-674, then compare callback behavior with src/txaio/aio.py:492-507. Start by adding tests for the three reported scenarios on both Twisted and asyncio. Done means the scenarios match across backends, the comments hold, and the behavior change is documented in the changelog.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

Summary

The join sequence in ApplicationSession is written as a chain of txaio.add_callbacks() on one future (src/autobahn/wamp/protocol.py:660-685, master 3f04432b). That only works on Twisted. txaio has no callback chaining on asyncio, so there the steps run as independent callbacks of the same future.

As a result, the behaviour promised by the code comments holds on Twisted but not on asyncio:

  • protocol.py:95: "ready", # after onJoin and all 'join' listeners have completed
  • protocol.py:668: # this should run regardless (on running onJoin)
  • protocol.py:674: # ignore any errors from onJoin (XXX or, should that be fatal?)

Observed behaviour

I drove a real ApplicationSession on both backends with a fake transport and a Welcome message. onJoin was asynchronous in all cases, and _swallow_error was overridden to record its calls. Autobahn 26.10.2 (the block above is identical on master), txaio master d291d39, CPython 3.11.

Scenario Twisted asyncio
Normal join onJoin finishes, then ready ready fires before onJoin finishes
A join observer raises error logged, onJoin runs, then ready onJoin never runs, ready never fires
onJoin raises error logged, then ready ready has already fired; the error reaches no handler

Recorded event order:

tw  normal:               onJoin-start, onJoin-end, ready
aio normal:               onJoin-start, ready, onJoin-end

tw  join_observer_raises: swallow "While notifying 'join'", onJoin-start, onJoin-end, ready
aio join_observer_raises: swallow "While notifying 'join'", swallow "While firing onJoin", swallow "While notifying 'ready'"

tw  onjoin_raises:        onJoin-start, onJoin-raises, swallow "While firing onJoin", ready
aio onjoin_raises:        onJoin-start, ready, onJoin-raises, "Task exception was never retrieved"

In the second asyncio case, the While firing onJoin and While notifying 'ready' messages are misleading. Neither onJoin nor ready ran: all three errbacks saw the same join observer error.

Why

On asyncio, add_callbacks() (txaio src/txaio/aio.py:492-507) adds a separate done-callback, and each one sees the original result of the future:

  • A value or future returned by a callback is dropped. The as_future(self.onJoin, ...) returned at protocol.py:671 isn't waited for, and nothing observes its failure.
  • An errback doesn't recover. So the failed fire("join", ...) future skips the onJoin callback, although the errback before it handled the error.

txaio removed its asyncio chaining emulation in 2.8.0 (2017), and its docs say not to depend on chaining. See crossbario/txaio#86.

join observers themselves are waited for on both backends, because fire() gathers them. So from reading the code, a Component with on_join / on_ready handlers should get ready after its join handlers; the affected case there is a failing join handler. I tested ApplicationSession only.

Impact

asyncio applications can't rely on ready to mean that the session has finished onJoin, and an exception in an asynchronous onJoin doesn't go through _swallow_error / onUserError.

Behaviour change to decide on

Making asyncio match Twisted has a side effect: ready then waits for onJoin, so if onJoin never returns (a long-running loop, which is a common asyncio pattern), ready never fires. That's already the case on Twisted, but it would be new on asyncio. Applications that listen for ready and also run a loop in onJoin would change behaviour, so this needs a changelog entry marked as a behaviour change.

Open questions

  1. Where should the fix go: in autobahn, as a join sequence that doesn't depend on chaining, or in a new txaio helper that both backends implement the same way?
  2. Should a failing onJoin stay non-fatal? The XXX comment at protocol.py:674 asks this already.
  3. Are the other add_callbacks() chains affected? There are 40 calls in src/autobahn outside the tests, and protocol.py:695-761 (around onWelcome, onLeave and onChallenge) has more of them. I haven't checked those. Crossbar.io runs on Twisted only, so its own calls aren't affected, but anything it uses from autobahn on Twisted must not change.

Testing (GREEN-RED-GREEN)

  • RED: tests for the three scenarios that run on both backends. They pass on Twisted and fail on asyncio.
  • GREEN: the fix; the same tests pass on both, on CPython and PyPy.

Done when

  • The three scenarios behave the same on Twisted and asyncio, covered by tests on both.
  • The comments at protocol.py:95 and :668 are true on both backends.
  • The behaviour change is in the changelog.

Note: This issue was drafted with AI assistance (Claude Code).

Lenguaje dominante
Python
Estrellas
2.5k
Forks
766
Merge medio
4 h 24 min
PR fusionados (30 d)
20

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de crossbario/autobahn-python

Todos los issues de crossbario/autobahn-python

Issues similares

Más issues de Python

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.