TIP 332: Half-Close for Bidirectional Channels

Author:         Alexandre Ferrieux <[email protected]>
State:          Final
Type:           Project
Vote:           Done
Created:        25-Sep-2008
Keywords:       Tcl,channel,close,socket,shutdown
Obsoletes:      301
Tcl-Version:    8.6
Tcl-Ticket:     219159


This TIP proposes to extend the close/chan close commands to let them perform an unidirectional "half-close" on bidirectional channels.


Bidirectional channels (sockets and command pipelines) allow Tcl to make an efficient use of a "filter process", by exchanging data back and forth over an abstract "single" channel.

However, this single channel abstraction comes with a too coarse-grained close primitive. Indeed, it closes both directions simultaneously, while it is often desirable to close "gracefully" the half-connection to the filter process, leaving the return path open. The effect of such a half-close is that the filter receives a bona fide EOF alone, without a nearly simultaneous SIGPIPE on its write end if it happens to be writing at that time. Moreover, if the filter is itself comprised of a pipeline of processes, some of which doing buffered I/O, then this graceful EOF may be the only way of flushing the chain and receiving back precious data.

This technique is supported by all modern OSes: for pipes there are actually two separate file descriptors/handles, and it suffices to close() the write side; for sockets, a single fd is used, but a specific syscall, shutdown(), brings back the ability to half-close. Hence it is fairly natural for a universal "OS glove" like Tcl to expose this universal feature.

Proposed Change

This TIP proposes to extend the close and twin brother chan close commands to take an optional extra "direction" argument, indicating a half-close on the substream going in that direction:

close channel ?read|write?

When the extra direction argument (which may be abbreviated) is given, first the OS-level half-close is performed: this means a shutdown() on a socket, and a close() of one end of a pipe for a command pipeline. Then, the Tcl-level channel data structure is either kept or freed depending on whether the other direction is still open:

	set f [open |command r+]
	close $f w ;# $f still exists
	close $f r ;# now $f is gone

Also, a single-argument close on an already half-closed bi-channel is defined to just "finish the job", which allows to write blind cleanup procedures easily:

	if {[catch {
	  set f [open |command r+]
	  close $f w
	} err]} {
	  close $f ;# close what's left

In the case of a command pipeline, the child-reaping duty falls upon the shoulders of the last close or half-close, so that an error condition at this stage (like "Child exited abnormally") doesn't leak system resources.

Last, a half-close on an already closed half raises an error:

	set [open |command r+]
	close $f w
	close $f w
	==> channel "file3" wasn't opened for writing

And the same applies to wrong-sided unidirectional channels:

	set [open filename r]
	close $f w
	==> channel "file3" wasn't opened for writing


The concept has gone full circle. From an initial half-close proposal very close to the current one, an ambitious chan split generalization was born in the surrounding enthusiasm, and specified in [301]. Then, [304]'s chan pipe was accepted, which addressed most of the "splitting" demand (asymmetric fconfigures on both ends of a command pipeline can be done by redirecting to a standalone pipe). Moreover, in hindsight it appears that the implementation of [301] had very long-ranging effects on various channel implementations, which are more numerous today than before (TLS, reflection API).

As a consequence, [301] is being withdrawn, and the current TIP goes back to its initial, lower profile: just provide the half-close.

C Interface

Luckily, the polymorphic channel API in Tcl has been fitted with a half-close function for nearly 10 years (!): the Tcl_ChannelType structure has had a close2proc member since 1998 in order to avoid deadlocks when closing a command pipeline. As a consequence, the implementation of half-close can be done with a constant channel ABI, avoiding any compatibility issue for extensions.

Still, the close2proc member does not have public status. To promote symmetry between the script-level and public C APIs, this TIP proposes to add an entry in the main stub table with the following signature:

int Tcl_CloseEx(Tcl_Interp *interp, Tcl_Channel chan, int flags)

The behavior being defined as mirrorring the script-level semantics described bove, where flags is either 0 (meaning bidirectional close) or one of TCL_CLOSE_READ and TCL_CLOSE_WRITE (meaning half-close).

Reference Implementation

See Patch 219159 https://sourceforge.net/tracker/index.php?func=detail&aid=219159&group_id=10894&atid=310894 .

After a discussion with Andreas Kupries, the plan is to half-close-enable only raw (unstacked) channels for the time being, raising an explicit error when trying a half-close on a non-bottom channel. This leaves time to carefully design the necessary API extension of the generic stacking and reflection layers, while preparing a fully compatible change (error cases becoming valid).

On the channel-type-dependent side, only sockets and pipelines will be half-close-enabled for now: these are assumed to represent the most pressing demand.


This document has been placed in the public domain.