Skip to content

Having a channel receive writeEOF makes it become a zombie channel -- writes don't fail, channel doesn't close #3294

@weissi

Description

@weissi

Expected behavior

I don't get zombie channels

Actual behavior

I get a zombie channel where the writes just get forgotten that doesn't have an input or output.

Steps to reproduce

  1. Create a NIOPipeChannel with just an output to a pipe (support half-closure, close input)
  2. Have it write a load (maybe 32 MB) to that pipe (DO NOT READ IT)
  3. Now close that pipe on the read end
  4. The following things should happen now (but they don't)
    a. I should get a channel error
    b. My writes should fail
    c. I should probably get a user event about that

SwiftNIO version/commit hash

2.83.0

System & version information

Linux

Trace

Here's a commented trace (strace, my debug output and DebugChannelHandlers mixed)

the registrations of fd 25

=== CHANNEL INBOUND (PipeChannel { PipePair { in=nil, out=Optional(SelectableFileHandle(isOpen: true, fd: 25)) }, active = false, localAddress = nil, remoteAddress = nil }) registered
[pid 17062] epoll_ctl(19, EPOLL_CTL_ADD, 25, {events=EPOLLERR|EPOLLHUP, data={u32=25, u64=4294967321}}) = 0
=== CHANNEL INBOUND (PipeChannel { PipePair { in=nil, out=Optional(SelectableFileHandle(isOpen: true, fd: 25)) }, active = true, localAddress = nil, remoteAddress = nil }) active
[pid 17062] epoll_ctl(19, EPOLL_CTL_MOD, 25, {events=EPOLLERR|EPOLLHUP, data={u32=25, u64=4294967321}}) = 0
=== CHANNEL OUTBOUND (PipeChannel { PipePair { in=nil, out=Optional(SelectableFileHandle(isOpen: true, fd: 25)) }, active = true, localAddress = nil, remoteAddress = nil }) close(mode: NIOCore.CloseMode.input)
== SHUT RD
=== CHANNEL INBOUND (PipeChannel { PipePair { in=nil, out=Optional(SelectableFileHandle(isOpen: true, fd: 25)) }, active = true, localAddress = nil, remoteAddress = nil }) userInboundEventTriggered(event: NIOCore.ChannelEvent.inputClosed)
[pid 17062] epoll_pwait(19, [{events=EPOLLIN, data={u32=20, u64=18446744069414584340}}], 64, -1, NULL, 8) = 1
C WRITING [474f0a](3 bytes) to PipeChannel { PipePair { in=nil, out=Optional(SelectableFileHandle(isOpen: true, fd: 25)) }, active = true, localAddress = nil, remoteAddress = nil }
strace: Process 17067 attached
=== CHANNEL OUTBOUND (PipeChannel { PipePair { in=nil, out=Optional(SelectableFileHandle(isOpen: true, fd: 25)) }, active = true, localAddress = nil, remoteAddress = nil }) close(mode: NIOCore.CloseMode.input)
=== CHANNEL OUTBOUND (PipeChannel { PipePair { in=nil, out=Optional(SelectableFileHandle(isOpen: true, fd: 25)) }, active = true, localAddress = nil, remoteAddress = nil }) write(data: (ByteBuffer: [474f0a](3 bytes)))
=== CHANNEL OUTBOUND (PipeChannel { PipePair { in=nil, out=Optional(SelectableFileHandle(isOpen: true, fd: 25)) }, active = true, localAddress = nil, remoteAddress = nil }) flush
write(pointer:) 107
C FUTURE WRITE success()

so far it's all good, we're written 3 bytes into that channel. Now we're gonna write 16 MiB

[pid 17062] epoll_pwait(19, C WRITE [474f0a](3 bytes) to PipeChannel { PipePair { in=nil, out=Optional(SelectableFileHandle(isOpen: true, fd: 25)) }, active = true, localAddress = nil, remoteAddress = nil } DONE
C WRITING [4242424242424242424242424242424242424242424242424242424242424242...4242424242424242424242424242424242424242424242424242424242424242](167772160 bytes) to PipeChannel { PipePair { in=nil, out=Optional(SelectableFileHandle(isOpen: true, fd: 25)) }, active = true, localAddress = nil, remoteAddress = nil }
[{events=EPOLLIN, data={u32=20, u64=18446744069414584340}}], 64, -1, NULL, 8) = 1
=== CHANNEL OUTBOUND (PipeChannel { PipePair { in=nil, out=Optional(SelectableFileHandle(isOpen: true, fd: 25)) }, active = true, localAddress = nil, remoteAddress = nil }) write(data: (ByteBuffer: [4242424242424242424242424242424242424242424242424242424242424242...4242424242424242424242424242424242424242424242424242424242424242](167772160 bytes)))
=== CHANNEL INBOUND (PipeChannel { PipePair { in=nil, out=Optional(SelectableFileHandle(isOpen: true, fd: 25)) }, active = true, localAddress = nil, remoteAddress = nil }) writabilityChanged(isWritable: false)
=== CHANNEL OUTBOUND (PipeChannel { PipePair { in=nil, out=Optional(SelectableFileHandle(isOpen: true, fd: 25)) }, active = true, localAddress = nil, remoteAddress = nil }) flush
write(pointer:) 107
write(pointer:) 107

cool, as expected the writabilityChanged(isWritable: false)

[pid 17062] epoll_ctl(19, EPOLL_CTL_MOD, 25, {events=EPOLLOUT|EPOLLERR|EPOLLHUP, data={u32=25, u64=4294967321}}) = 0
[... uninteresting stuff ...]

Now, we're seeing the pipe close

sleep 17070 should be dead
 <unfinished ...>
[pid 17070] +++ killed by SIGKILL +++
[pid 17062] <... epoll_pwait resumed>[{events=EPOLLHUP, data={u32=28, u64=18446744069414584348}}, {events=EPOLLERR, data={u32=25, u64=4294967321}}], 64, -1, NULL, 8) = 2

See just above? events=EPOLLERR, data={u32=25, we're getting EPOLLERR for fd 25.

And we react (closing the pipe, the SelectableFileHandle etc) driven from writeEOF

1834
[pid 17062] epoll_ctl(19, EPOLL_CTL_MOD, 28, {events=EPOLLIN|EPOLLERR|EPOLLHUP, data={u32=28, u64=18446744069414584348}}) = 0
read(pointer:) 127
[pid 17062] epoll_ctl(19, EPOLL_CTL_DEL, 28, 0xffffb200c268) = 0
--- PIPE CLOSE Optional(SelectableFileHandle(isOpen: true, fd: 28)), nil
--- CLOSING 28 SelectableFileHandle(isOpen: false, fd: -1)
=== CALL BEGIN to writeEOF() on PipeChannel { PipePair { in=nil, out=Optional(SelectableFileHandle(isOpen: true, fd: 25)) }, active = true, localAddress = nil, remoteAddress = nil }
[pid 17062] epoll_ctl(19, EPOLL_CTL_DEL, 25, 0xffffb200cf98) = 0
--- CLOSING 25 SelectableFileHandle(isOpen: false, fd: -1)
=== CALL END to writeEOF() on PipeChannel { PipePair { in=nil, out=Optional(SelectableFileHandle(isOpen: true, fd: 25)) }, active = true, localAddress = nil, remoteAddress = nil }

and then: Nothing, no channel events, no failing futures, nothing.

And then, once a second I see (because I made a repeated task printing the channel)

C ALIVE PipeChannel { PipePair { in=nil, out=Optional(SelectableFileHandle(isOpen: true, fd: 25)) }, active = true, localAddress = nil, remoteAddress = nil }

Note the other weirdness: The PipePair seems to cache the string, so it shows SelectableFileHandle(isOpen: true, fd: 25) despite the fact that the SelectableFileHandle knows it's closed

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind/bugFeature doesn't work as expected.platform/linuxLinux platform specific issues.size/MMedium task. (A couple of days of work.)

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions