Tcl Source Code

Rationale for rollback of refchan event generation in core
Login

All,

Below is my rationale for requesting a rollback of certain post-8.6 changes related to event generation for refchans. Thanks to Jan, the experimental branch bug-18f4a94d03 contains the rollback.

In summary, the changes

The below merely consolidates the discussion in various related tickets. In (roughly) chronological order:

Commit e066e24f

The commit changed the chan postevent from synchronous to asynchronous operation as a fix for ticket 67a5eabb.

TL;DR This change was unnecessary and if at all shown to be so, should have absolutely required a TIP.

For starters, the test case motivating the fix is itself flawed. The coroutine busy error was caused by the refchan implementation directly invoking chan postevent which results in a nested coroutine invocation. For these use cases, in particular where the refchan implementation is non-reentrant, it has always been the case that refchan implementations post events using the after idiom as described in Andreas' 2009 Tcl conference paper. As @aspect noted in the same ticket this is also true of the tcllib refchan implementations. It also is true of TWAPI's TLS implementation.

This change resulted in @bll logging ticket 18f4a9 illustrating a failure in previously working code. @pooryorick marked this as invalid because event loop was not running (as per my understanding). Without commenting on the merits either way, it is still the case that working refchan implementations that correctly used the after idiom will now have events passed through the event queue additional times for no good reason.

In any case, a change in a command behavior from synchronous or asynchronous should require a TIP. Paraphrasing @dgp's post, this results in (a) loss of the ability to choose synchronous and asynchronous operation, and (b) an incompatible loss of function in a public command.

Commit d0dd6d19

As a fix for de232b49, buffered coroutine write events not called, the commit introduced the continuous generation (roughly equivalent of after 0 but at the C level) of write events (when a write event handler is registered on a non-blocking channel) within the channel core implementation.

TL;DR Again, as in the previous commit, the change was unnecessary. It also resulted in new bugs.

As stated above, refchans are responsible of generation of their own events and the test example in the ticket is flawed in its implementation. It should have set up a timer itself at the script level as demonstrated in one of the attachments to that ticket. Instead, the commit changed the Tcl core itself to generate write events in a continuous 0-interval timer with no knowledge of the actual channel state. As a result,

  1. Test socket-14.11.0 started failing with this commit, sporadically on Unix as reported by @dgp, and consistently on Windows. Root cause was the fileevent handler being called before the connection was actually open.

  2. Ticket 080f846f, test failures in TWAPI TLS. Same root cause as above.

  3. The Tk UI freeze subsequently logged at ac7592e7 resulting from the continuous placement of writable events on the event queue.

Attempts were made to fix the above as summarized below but the bigger issue is that this is fundamentally broken design for more than one reason.

@pooryorick has proposed fixes for the specific bugs listed above.

In any case, aside from these misgivings about these fixes, more concerning is that these are point fixes and neither does anything to correct the fundamental point that continuous timer based generation of I/O events completely defeats the intent of event based I/O.

Rebuttals

@pooryorick has objected to reverting. See his comments and my responses in the various tickets linked above.

One additional point I want to make about fundamental motivation for the commits. Either

or

Andreas Kupries' comments on the core list in support of reverting

[This response collects quotes from multiple mails received from Nathan]

> Hi everyone,
 
> A number of people, including apparently several members of the Tcl
> Core Team, have decided that [chan postevent] is part of Tcl's
> synchronous API for channels rather part of Tcl's asynchronous API
> for channels, and on that basis want to close the following issue
> reports
 
> 	https://core.tcl-lang.org/tcl/tktview/67a5eabbd3d195915d3ccc13246bdecf10e20726
> 	https://core.tcl-lang.org/tcl/tktview/de232b49f26da1c18e07513d4c7caa203cd27910
 
> The idea that [chan postevent] is part of Tcl's synchronous API is
> one of the most wrong-headed ideas I've encountered among the
> contributors to Tcl.

Strong claim. Emotional argument also, not technical.

> I'm sure that at least some people reading this have experienced
> failure when trying to implement a refchan, and as a result have
> simply turned their backs on refchans and found some other way to
> get the job done.  When [chan postevent] is part of Tcl's
> asynchronous API,

> as was always intended,

You seem to claim telepathy as well. Specifically that of my mind,
given that I was the writer of [TIP #219][1] back in 2004.

[1] https://core.tcl-lang.org/tips/doc/trunk/tip/219.md

Despite the distance of 20 years I am pretty sure that I had not
intended async operation.

And coming back into things after reading more mails I am now fully
sure it was not intended to be async. See later where I talk about
what a `refchan` truly is. A channel driver, just in Tcl. Operating
under the same rules as a C-level driver. Whose equivalent to
`postevent` is, yes, verily, synchronous.

> you wouldn't have experienced that "hanging" behaviour of refchans.
 
> For all those who have been bitten by refchans, now would be a good
> time to voice your support for fixing bugs keep it from being
> asynchronous as it should be.
 
> Here is a list of all wiki pages containing examples of postevent
> that are broken until the issues referenced above are fixed:
 
> 	https://wiki.tcl-lang.org/page/fifo2+in+pure+Tcl
> 	https://wiki.tcl-lang.org/page/reflected+channel+example
> 	https://wiki.tcl-lang.org/page/reflected+channel
 
> Here is a list of wiki pages that contain ugly hacks to work around
> the issues reference above:
 
> 	https://wiki.tcl-lang.org/page/A+template+reflected+channel
> 	https://wiki.tcl-lang.org/page/topcua

The hack you refer too are the `after ...` constructions used to push
`postevent` calls out of non-reentrant parts of the procs, right ?

I do not see such as hacks, but rather as the standard way to avoid
reentrancy troubles anywhere.
 
> The general principle here is that event handlers should not
> recursively enter event-handling, but should instead allow the event
> loop to mediate.  This should be obvious, so I don't know why we now
> have to debate it for [chan postevent].

> Was it ever documented to be synchronous?

No. Of course not. Because synchronous behaviour is the default for
pretty much all Tcl commands, and it is the __exceptions__ from that
default which are documented.

> Did any change I made require a change in the documentation?

Changing a command from sync to async is a strong change of the
semantics. Yes, that requires a change of the docs. Because, see
above, the async behaviour of a command is the exception from the
default.

>If not, no TIP is required to fix bugs in the implementation.

Just declaring something as a bug does not give you a
free-to-change-this card.

> Yes, really.  [chan postevent] is the mechanism provided for a refchan
> to notifiy Tcl of events on the channel.  At the C level, the mechanism
> provided to an event source for this purpose is Tcl_QueueEvent(), which

Actually the C level equivalent of `postevent` is `Tcl_NotifyChannel()`
which is very much synchronous. Somehow the C-level channel drivers
still manage to work.

https://www.tcl-lang.org/man/tcl8.6/TclLib/CrtChannel.htm

	Tcl_NotifyChannel is called by a channel driver to indicate to
        the generic layer that the events specified by mask have
        occurred on the channel. Channel drivers are responsible for
        invoking this function ___whenever the channel handlers need to
        be called for the channel___ (or other pending tasks like a write
        flush should be performed). See WATCHPROC below for more
        details.

(WATCHPROC) https://www.tcl-lang.org/man/tcl8.6/TclLib/CrtChannel.htm#M16

	... the channel driver is responsible for calling
	Tcl_NotifyChannel to inform the generic channel module. The
	driver should take care not to starve other channel drivers or
	sources of callbacks by invoking Tcl_NotifyChannel too
	frequently. Fairness can be insured by using the Tcl event
	queue to allow the channel event to be scheduled in sequence
	with other events

So, `Tcl_NC` is synchronous in C, and C-based channel driver are
responsible for using the C-level equivalent of `after` to explicitly
go through the event queue where/when needed, i.e. to avoid reentrancy
issues, etc. And these drivers __do so__.

A `refchan` is a __direct__ mapping of a channel driver into Tcl, and
therefore has the __same responsibilities__ as a C-level driver, up to
and including to explicitly go through the event queue when needed,
using `after` as the effective entrypoint to `Tcl_QueueEvent`.

A refchan directly talks to the low-level Tcl's IO system like C
drivers to, and has to follow the same rules as they do.


> My answer is that [chan postevent] was always sufficiently
> documented as asynchronous,

I see no such documentation in the `chan` manpage, nor in the TIP 219.
Please provide a reference to the docs you believe made the async
claim.

Poor Yorick's response

whether [chan postevent] was intended to be asynchronous, you had to
look decades back into the past and reconstruct your memories in light
of the current debate, and you have persuaded yourself that [chan
postevent] was intended to be synchronous.  I'll now attempt to persuade
you of the opposite.

In the small, Tcl_NotifyChannel() appears to be synchronous, but when
one steps back and looks at the larger picture, it is part of an
asynchronous operation.  It's true that Tcl_NotifyChannel() does not
queue up an event, but that's because Tcl_NotifyChannel() is itself an
event handler.  In tclUnixChan.c, which is the implementation of the
*nix channel driver for files, FileWatchProc() passes
Tcl_NotifyChannel() to Tcl_CreateFileHandler(), and the notifier then
calls Tcl_NotifyChannel() later when it detects an event on the channel.
This effectively makes Tcl_NotifyChannel() asynchronous, as its only
caller is the event loop itself.  When this also true for refchans, i.e.
when the only caller of a handler for an event on the refchan is the
event loop itself, then the operation of refchan is consistent with the
operation of other channels.  Unfortunately, due to the reversions
listed below, this is no-longer the case.

You named it [chan postevent] instead of [chan notify] because you
intuited that it was not at the same level as Tcl_NotifyChannel(), even
if you didn't articulate then, or even now.  [chan postevent] is not
Tcl_NotifyChannel().  It operates at an even lower layer:  It is the
mechanism that informs the notifier that there is an event on the
channel.  File event detection is not part of the channel driver, but
rather is a part of the notifier:  select() is called in tclUnixNotfy.c,
not in tclUnixChan.c.  In contrast with other event sources that are
surfaced through select(), which interacts with external systems during
normal event loop operations, a refchan must have some way to make the
event known to Tcl.  [chan postevent] was provided for that purpose.  In
other words, [chan postevent] is the analogue of select(), not the
analogue of Tcl_NotifyChannel().  As such, it should return something to
the notifier, like select() does during normal event loop processing.
The way to have a refchan do that is for [chan postevent] to place an
event on the queue, just as its name implies.  So you got the name
right, even if you weren't sure exactly why.

Requiring every refchan to use [after] to call [chan postevent] is a
completely unnecessary wart.  It's nothing more than leakage of poor
implementation into the script level.  Inspection of the design proves
that it is unnecessary, and not one example has been brought forward
demonstrating any reason a refchan might need to call [chan postevent]
synchronously when a channel is nonblocking.  No one out there is doing
it either, because it would result in sporadic deadlocks.  This is
simply hand-wringing over a fictitious situation.  Since a refchan must
use [after] to call [chan postevent] if it wants to work properly,
taking care of that part at the implementation level is a much better
strategy.

For the last five years, the following bugs have been fixed on
core-8-branch and in trunk

    https://core.tcl-lang.org/tcl/tktview/de232b49f26da1c18e07513d4c7caa203cd27910

    https://core.tcl-lang.org/tcl/tktview/67a5eabbd3d195915d3ccc13246bdecf10e20726

Throughout five years of development on those branches, the fix has
never been called into question, and has never lead to another valid bug
report (at least not one that wasn't quickly fixed).  Before coroutines,
calling [chan postevent] without a timer "only" lead to deadlocks.
Nowadays, when a refchan is implemented as a coroutine, it is an error
to call [chan postevent] synchronously.  I'll repeat that:

    It is an error to call [chan postevent] synchronously.

Coroutine implementations of refchans make it manifestly clear that
[chan postevent] must be asynchronous.  Therefore, there is no point in
trying to pretend that [chan postevent] is synchronous.  It must go
through the event queue, and the only way currently to do that is to use
[after].  Although it is coroutine implementations of refchans that make
this obvious, it is and always has been true for all other
implementations as well.  It's just that with other implementations, the
error can be more camouflaged, with deadlocks happening at unpredictable
times.

But now, via the following commits, the fix for the bugs mentioned
above has been undone.

    core-8-branch

        https://core.tcl-lang.org/tcl/info/8d4d978bd3ca580d

    trunk

        https://core.tcl-lang.org/tcl/info/1ec9927351b255fb

This work was tedious and painstaking, it solved the bugs referenced
above, and it made the operation of refchans more consistent with other
types of channels. Now Tcl has lost this contribution, and all for a
"bug" report that doesn't actually demonstrate any reproducible bug, and
only because someone has some ideas but no code to stand those ideas on.
It's a mistake to undo this work, and it's a mistake to rationalize
undoing it by documenting [chan postevent] as synchronous.  Let's bring
back consistency with the design of the IO subsystem, ditch this
brain-dead idea that [chan postevent] should do anything other than post
a notification for the notifier, and revert the reversions of the code
that fixed the bugs above.

-- 
Yorick