detached 182dc8b

perf(fdwait): pin long-lived fds and wake waits without timeouts

Pin registers a descriptor once for the poller's lifetime, so a Poll
on pinned fds makes no epoll_ctl call and allocates nothing. A pinned
eventfd lets Wake end a wait from any goroutine, and StopOn ends every
Poll once a context is done, so loops need no polling timeout to see
work or shutdown. The readiness check runs under the wake file's
reference, so a concurrent Close cannot hand its number to a new file.
A ready pinned fd that a Poll does not list fails it instead of spinning.
commit
182dc8b0c1ec67561041e70be4c76a7539aae29f
author
brice · · 2026-10-14 04:06 UTC
committer
brice · · 2026-10-14 04:35 UTC
parent
bda62c7
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +348 -72
M internal/adapters/fdwait/edges_test.go +125 -1
@@ -1,11 +1,133 @@
11 package fdwait
22
33 import (
4+ "context"
5+ "errors"
46 "testing"
7+ "time"
58
69 "golang.org/x/sys/unix"
710 )
811
12+// A pinned fd is reported like a transient one, costs no epoll call per
13+// Poll, and a ready pinned fd a Poll does not list fails it instead of
14+// spinning.
15+func TestPollPinned(t *testing.T) {
16+ p, err := New()
17+ if err != nil {
18+ t.Fatal(err)
19+ }
20+ defer p.Close()
21+ pinned, other := eventfd(t), eventfd(t)
22+ if err := p.Pin(int32(pinned)); err != nil {
23+ t.Fatal(err)
24+ }
25+ fds := []unix.PollFd{{Fd: int32(pinned), Events: unix.POLLIN}}
26+ if n, err := p.Poll(fds, 1); n != 0 || err != nil {
27+ t.Fatalf("idle: %d %v", n, err)
28+ }
29+ go unix.Write(pinned, []byte{1, 0, 0, 0, 0, 0, 0, 0})
30+ if n, err := p.Poll(fds, 1000); n != 1 || err != nil || fds[0].Revents&unix.POLLIN == 0 {
31+ t.Fatalf("pinned ready: %d %v %+v", n, err, fds)
32+ }
33+ // Still registered after Poll returns: it is pinned.
34+ if err := p.raw.Control(func(ep uintptr) {
35+ var events [2]unix.EpollEvent
36+ if n, err := unix.EpollWait(int(ep), events[:], 0); n != 1 || err != nil || events[0].Fd != int32(pinned) {
37+ t.Errorf("pinned registration: %d %v %+v", n, err, events[:n])
38+ }
39+ }); err != nil {
40+ t.Fatal(err)
41+ }
42+ if _, err := p.Poll([]unix.PollFd{{Fd: int32(other), Events: unix.POLLIN}}, 1000); err == nil {
43+ t.Fatal("Poll without a ready pinned fd succeeded")
44+ }
45+}
46+
47+// Polling only pinned fds allocates nothing and makes no epoll_ctl call.
48+func TestPollPinnedAllocations(t *testing.T) {
49+ p, err := New()
50+ if err != nil {
51+ t.Fatal(err)
52+ }
53+ defer p.Close()
54+ fd := eventfd(t)
55+ if err := p.Pin(int32(fd)); err != nil {
56+ t.Fatal(err)
57+ }
58+ fds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}}
59+ signal(t, fd)
60+ p.Poll(fds, -1)
61+ if allocs := testing.AllocsPerRun(100, func() { p.Poll(fds, -1) }); allocs != 0 {
62+ t.Errorf("pinned ready Poll: %.1f allocs, want 0", allocs)
63+ }
64+}
65+
66+// Wake ends a wait without a timeout and returns no events; wakes before a
67+// Poll coalesce into one early return.
68+func TestPollWake(t *testing.T) {
69+ p, err := New()
70+ if err != nil {
71+ t.Fatal(err)
72+ }
73+ defer p.Close()
74+ fds := []unix.PollFd{{Fd: int32(eventfd(t)), Events: unix.POLLIN}}
75+ done := make(chan error, 1)
76+ go func() {
77+ n, err := p.Poll(fds, -1)
78+ if err == nil && n != 0 {
79+ err = errors.New("woken poll reported events")
80+ }
81+ done <- err
82+ }()
83+ p.Wake()
84+ select {
85+ case err := <-done:
86+ if err != nil {
87+ t.Fatal(err)
88+ }
89+ case <-time.After(5 * time.Second):
90+ t.Fatal("Wake did not end the wait")
91+ }
92+ p.Wake()
93+ p.Wake()
94+ if n, err := p.Poll(fds, 1000); n != 0 || err != nil {
95+ t.Fatalf("coalesced wake: %d %v", n, err)
96+ }
97+ if n, err := p.Poll(fds, 1); n != 0 || err != nil {
98+ t.Fatalf("second wake was not coalesced: %d %v", n, err)
99+ }
100+}
101+
102+// After the StopOn context ends, a running and every later Poll return at
103+// once, also with no timeout.
104+func TestPollStopOn(t *testing.T) {
105+ p, err := New()
106+ if err != nil {
107+ t.Fatal(err)
108+ }
109+ defer p.Close()
110+ ctx, cancel := context.WithCancel(context.Background())
111+ p.StopOn(ctx)
112+ fds := []unix.PollFd{{Fd: int32(eventfd(t)), Events: unix.POLLIN}}
113+ done := make(chan error, 1)
114+ go func() { _, err := p.Poll(fds, -1); done <- err }()
115+ cancel()
116+ select {
117+ case err := <-done:
118+ if err != nil {
119+ t.Fatal(err)
120+ }
121+ case <-time.After(5 * time.Second):
122+ t.Fatal("cancel did not end the wait")
123+ }
124+ for range 3 {
125+ if n, err := p.Poll(fds, -1); n != 0 || err != nil {
126+ t.Fatalf("poll after stop: %d %v", n, err)
127+ }
128+ }
129+}
130+
9131 func eventfd(t *testing.T) int {
10132 t.Helper()
11133 fd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
@@ -121,7 +243,9 @@ func TestPollFailsAfterLostRegistration(t *testing.T) {
121243 t.Fatal(err)
122244 }
123245 unix.Close(fd) // what a caller closing fd during a wait does
124- p.unwatch([]unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}})
246+ p.fds, p.added = []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}}, 1
247+ p.unwatch()
248+ p.fds = nil
125249 other := eventfd(t)
126250 if _, err := p.Poll([]unix.PollFd{{Fd: int32(other), Events: unix.POLLIN}}, 1); err == nil {
127251 t.Fatal("Poll succeeded with a registration it could not remove")
@@ -1,11 +1,133 @@
1 package fdwait1 package fdwait
2 2
3 import (3 import (
4+ "context"
5+ "errors"
4 "testing"6 "testing"
7+ "time"
5 8
6 "golang.org/x/sys/unix"9 "golang.org/x/sys/unix"
7 )10 )
8 11
12+// A pinned fd is reported like a transient one, costs no epoll call per
13+// Poll, and a ready pinned fd a Poll does not list fails it instead of
14+// spinning.
15+func TestPollPinned(t *testing.T) {
16+ p, err := New()
17+ if err != nil {
18+ t.Fatal(err)
19+ }
20+ defer p.Close()
21+ pinned, other := eventfd(t), eventfd(t)
22+ if err := p.Pin(int32(pinned)); err != nil {
23+ t.Fatal(err)
24+ }
25+ fds := []unix.PollFd{{Fd: int32(pinned), Events: unix.POLLIN}}
26+ if n, err := p.Poll(fds, 1); n != 0 || err != nil {
27+ t.Fatalf("idle: %d %v", n, err)
28+ }
29+ go unix.Write(pinned, []byte{1, 0, 0, 0, 0, 0, 0, 0})
30+ if n, err := p.Poll(fds, 1000); n != 1 || err != nil || fds[0].Revents&unix.POLLIN == 0 {
31+ t.Fatalf("pinned ready: %d %v %+v", n, err, fds)
32+ }
33+ // Still registered after Poll returns: it is pinned.
34+ if err := p.raw.Control(func(ep uintptr) {
35+ var events [2]unix.EpollEvent
36+ if n, err := unix.EpollWait(int(ep), events[:], 0); n != 1 || err != nil || events[0].Fd != int32(pinned) {
37+ t.Errorf("pinned registration: %d %v %+v", n, err, events[:n])
38+ }
39+ }); err != nil {
40+ t.Fatal(err)
41+ }
42+ if _, err := p.Poll([]unix.PollFd{{Fd: int32(other), Events: unix.POLLIN}}, 1000); err == nil {
43+ t.Fatal("Poll without a ready pinned fd succeeded")
44+ }
45+}
46+
47+// Polling only pinned fds allocates nothing and makes no epoll_ctl call.
48+func TestPollPinnedAllocations(t *testing.T) {
49+ p, err := New()
50+ if err != nil {
51+ t.Fatal(err)
52+ }
53+ defer p.Close()
54+ fd := eventfd(t)
55+ if err := p.Pin(int32(fd)); err != nil {
56+ t.Fatal(err)
57+ }
58+ fds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}}
59+ signal(t, fd)
60+ p.Poll(fds, -1)
61+ if allocs := testing.AllocsPerRun(100, func() { p.Poll(fds, -1) }); allocs != 0 {
62+ t.Errorf("pinned ready Poll: %.1f allocs, want 0", allocs)
63+ }
64+}
65+
66+// Wake ends a wait without a timeout and returns no events; wakes before a
67+// Poll coalesce into one early return.
68+func TestPollWake(t *testing.T) {
69+ p, err := New()
70+ if err != nil {
71+ t.Fatal(err)
72+ }
73+ defer p.Close()
74+ fds := []unix.PollFd{{Fd: int32(eventfd(t)), Events: unix.POLLIN}}
75+ done := make(chan error, 1)
76+ go func() {
77+ n, err := p.Poll(fds, -1)
78+ if err == nil && n != 0 {
79+ err = errors.New("woken poll reported events")
80+ }
81+ done <- err
82+ }()
83+ p.Wake()
84+ select {
85+ case err := <-done:
86+ if err != nil {
87+ t.Fatal(err)
88+ }
89+ case <-time.After(5 * time.Second):
90+ t.Fatal("Wake did not end the wait")
91+ }
92+ p.Wake()
93+ p.Wake()
94+ if n, err := p.Poll(fds, 1000); n != 0 || err != nil {
95+ t.Fatalf("coalesced wake: %d %v", n, err)
96+ }
97+ if n, err := p.Poll(fds, 1); n != 0 || err != nil {
98+ t.Fatalf("second wake was not coalesced: %d %v", n, err)
99+ }
100+}
101+
102+// After the StopOn context ends, a running and every later Poll return at
103+// once, also with no timeout.
104+func TestPollStopOn(t *testing.T) {
105+ p, err := New()
106+ if err != nil {
107+ t.Fatal(err)
108+ }
109+ defer p.Close()
110+ ctx, cancel := context.WithCancel(context.Background())
111+ p.StopOn(ctx)
112+ fds := []unix.PollFd{{Fd: int32(eventfd(t)), Events: unix.POLLIN}}
113+ done := make(chan error, 1)
114+ go func() { _, err := p.Poll(fds, -1); done <- err }()
115+ cancel()
116+ select {
117+ case err := <-done:
118+ if err != nil {
119+ t.Fatal(err)
120+ }
121+ case <-time.After(5 * time.Second):
122+ t.Fatal("cancel did not end the wait")
123+ }
124+ for range 3 {
125+ if n, err := p.Poll(fds, -1); n != 0 || err != nil {
126+ t.Fatalf("poll after stop: %d %v", n, err)
127+ }
128+ }
129+}
130+
9 func eventfd(t *testing.T) int {131 func eventfd(t *testing.T) int {
10 t.Helper()132 t.Helper()
11 fd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)133 fd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
@@ -121,7 +243,9 @@ func TestPollFailsAfterLostRegistration(t *testing.T) {
121 t.Fatal(err)243 t.Fatal(err)
122 }244 }
123 unix.Close(fd) // what a caller closing fd during a wait does245 unix.Close(fd) // what a caller closing fd during a wait does
124- p.unwatch([]unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}})246+ p.fds, p.added = []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}}, 1
247+ p.unwatch()
248+ p.fds = nil
125 other := eventfd(t)249 other := eventfd(t)
126 if _, err := p.Poll([]unix.PollFd{{Fd: int32(other), Events: unix.POLLIN}}, 1); err == nil {250 if _, err := p.Poll([]unix.PollFd{{Fd: int32(other), Events: unix.POLLIN}}, 1); err == nil {
127 t.Fatal("Poll succeeded with a registration it could not remove")251 t.Fatal("Poll succeeded with a registration it could not remove")
M internal/adapters/fdwait/poller.go +223 -71
@@ -2,6 +2,7 @@
22 package fdwait
33
44 import (
5+ "context"
56 "errors"
67 "fmt"
78 "os"
@@ -12,16 +13,46 @@ import (
1213 "golang.org/x/sys/unix"
1314 )
1415
15-// Poller owns an epoll descriptor and is used by one goroutine; Close may
16-// also run from another goroutine to end a wait. Watched descriptors remain
17-// owned by the caller, who must not close them while a Poll waits.
16+// Poller owns an epoll descriptor and is used by one goroutine; Wake and
17+// Close may run from any goroutine. Watched descriptors remain owned by the
18+// caller, who must not close them while a Poll waits.
19+//
20+// Descriptors that live as long as the poller are registered once with Pin;
21+// others are registered for one Poll call. A Poll returns on a watched fd,
22+// on Wake, once the context given to StopOn ends, or on Close: a loop needs
23+// no polling timeout to notice work or shutdown.
1824 type Poller struct {
1925 file *os.File
2026 raw syscall.RawConn
27+ // wake is an eventfd pinned in the epoll set: Wake writes it.
28+ wake *os.File
29+ wakeRaw syscall.RawConn
30+ wakeFD int32
31+ ctx context.Context
32+ stop func() bool
33+ pinned []int32 // besides wakeFD
2134 // broken is set when a registration could not be removed: it may keep
2235 // the epoll descriptor readable forever, so every later Poll fails
2336 // instead of spinning.
2437 broken error
38+ // noDeadline: the read deadline is already cleared.
39+ noDeadline bool
40+
41+ // State of the running Poll, used by the callbacks below. They are
42+ // bound once in New, so a Poll allocates nothing.
43+ fds []unix.PollFd
44+ check []unix.PollFd
45+ added int
46+ ctlErr error
47+ n int
48+ pollErr error
49+ buf [8]byte
50+ addFn func(uintptr)
51+ delFn func(uintptr)
52+ checkFn func(uintptr)
53+ readFn func(uintptr) bool
54+ drainFn func(uintptr)
55+ kickFn func(uintptr)
2556 }
2657
2758 func New() (*Poller, error) {
@@ -33,108 +64,229 @@ func New() (*Poller, error) {
3364 unix.Close(fd)
3465 return nil, fmt.Errorf("fdwait epoll nonblock: %w", err)
3566 }
36- f := os.NewFile(uintptr(fd), "fdwait")
37- raw, err := f.SyscallConn()
38- if err != nil {
39- f.Close()
67+ p := &Poller{file: os.NewFile(uintptr(fd), "fdwait")}
68+ if p.raw, err = p.file.SyscallConn(); err != nil {
69+ p.file.Close()
4070 return nil, fmt.Errorf("fdwait epoll conn: %w", err)
4171 }
42- return &Poller{file: f, raw: raw}, nil
72+ efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
73+ if err != nil {
74+ p.file.Close()
75+ return nil, fmt.Errorf("fdwait wake eventfd: %w", err)
76+ }
77+ p.wake, p.wakeFD = os.NewFile(uintptr(efd), "fdwait-wake"), int32(efd)
78+ p.addFn, p.delFn, p.readFn, p.checkFn = p.watch, p.unwatchAll, p.ready, p.pollNow
79+ p.drainFn, p.kickFn = p.drain, p.kick
80+ if p.wakeRaw, err = p.wake.SyscallConn(); err == nil {
81+ err = p.control(unix.EPOLL_CTL_ADD, p.wakeFD)
82+ }
83+ if err != nil {
84+ p.Close()
85+ return nil, fmt.Errorf("fdwait wake: %w", err)
86+ }
87+ return p, nil
4388 }
4489
45-func (p *Poller) Close() error { return p.file.Close() }
90+// Close ends a running Poll and releases the epoll and wake descriptors.
91+func (p *Poller) Close() error {
92+ if p.stop != nil {
93+ p.stop()
94+ }
95+ err := p.file.Close()
96+ p.wake.Close()
97+ return err
98+}
99+
100+func (p *Poller) control(op int, fd int32) error {
101+ var err error
102+ if cerr := p.raw.Control(func(ep uintptr) {
103+ ev := unix.EpollEvent{Events: unix.EPOLLIN, Fd: fd}
104+ err = unix.EpollCtl(int(ep), op, int(fd), &ev)
105+ }); cerr != nil {
106+ return cerr
107+ }
108+ return err
109+}
110+
111+// Pin registers fd for every later Poll, which then costs it no epoll call.
112+// fd must stay open until Close, and every Poll must list it: a ready pinned
113+// fd that a Poll does not list fails that Poll.
114+func (p *Poller) Pin(fd int32) error {
115+ if err := p.control(unix.EPOLL_CTL_ADD, fd); err != nil {
116+ return fmt.Errorf("fdwait pin fd %d: %w", fd, err)
117+ }
118+ p.pinned = append(p.pinned, fd)
119+ return nil
120+}
121+
122+// StopOn makes every Poll return (0, nil) at once after ctx ends. Call it
123+// once, before polling.
124+func (p *Poller) StopOn(ctx context.Context) {
125+ p.ctx = ctx
126+ p.stop = context.AfterFunc(ctx, p.Wake)
127+}
128+
129+// Wake makes the running or next Poll return. Wakes before that Poll
130+// coalesce. Safe from any goroutine, also after Close.
131+func (p *Poller) Wake() { _ = p.wakeRaw.Control(p.kickFn) }
132+
133+func (p *Poller) kick(fd uintptr) {
134+ one := [8]byte{1}
135+ _, _ = unix.Write(int(fd), one[:])
136+}
137+
138+func (p *Poller) drain(fd uintptr) { _, _ = unix.Read(int(fd), p.buf[:]) }
139+
140+func (p *Poller) stopped() bool { return p.ctx != nil && p.ctx.Err() != nil }
46141
47142 // Poll waits like poll(2) for readable/error events on fds and fills their
48143 // Revents. It never consumes caller events. Negative fds are ignored and a
49144 // descriptor listed twice is watched once. timeout is in milliseconds; a
50-// negative one waits until an event or Close.
145+// negative one waits until an event, Wake, the end of the StopOn context or
146+// Close. A Wake or stop returns the events seen, possibly none.
51147 //
52-// Each call explicitly removes its epoll registrations before returning.
53-// Relying on close to remove them is unsafe: another fd or a kernel fence
54-// may retain the open file description and keep the epoll fd readable.
55-// When nothing is ready yet, a descriptor epoll cannot watch (EPERM,
56-// EINVAL, ELOOP for epoll nested too deep) fails the call.
148+// Each call removes the registrations it made before returning. Relying on
149+// close to remove them is unsafe: another fd or a kernel fence may retain
150+// the open file description and keep the epoll fd readable. When nothing is
151+// ready yet, a descriptor epoll cannot watch (EPERM, EINVAL, ELOOP for epoll
152+// nested too deep) fails the call.
57153 func (p *Poller) Poll(fds []unix.PollFd, timeout int) (int, error) {
58154 if p.broken != nil {
59155 return 0, p.broken
60156 }
61- // Ready descriptors need no registration, and an expired deadline
62- // would skip the readiness check in RawConn.Read.
63- if n, err := pollNow(fds); n != 0 || err != nil || timeout == 0 {
64- return n, err
65- }
66- var ctlErr error
67- added := 0
68- err := p.raw.Control(func(ep uintptr) {
69- for _, f := range fds {
70- if f.Fd >= 0 {
71- ev := unix.EpollEvent{Events: unix.EPOLLIN, Fd: f.Fd}
72- e := unix.EpollCtl(int(ep), unix.EPOLL_CTL_ADD, int(f.Fd), &ev)
73- if e != nil && !(errors.Is(e, unix.EEXIST) && listedBefore(fds, added)) {
74- ctlErr = fmt.Errorf("fdwait watch fd %d: %w", f.Fd, e)
75- return
76- }
77- }
78- added++
79- }
80- })
81- defer p.unwatch(fds[:added])
82- if err != nil {
83- return 0, err
157+ p.fds = fds
158+ defer func() { p.fds = nil }()
159+ transient := false
160+ for i := range fds {
161+ transient = transient || !p.skip(i)
84162 }
85- if ctlErr != nil {
86- return 0, ctlErr
163+ // An expired deadline skips RawConn.Read's own check, and a ready
164+ // transient fd needs no registration. Pinned fds are checked by Read.
165+ if (timeout == 0 || transient) && p.ready(0) || timeout == 0 {
166+ return p.n, p.pollErr
87167 }
88- if timeout > 0 {
89- err = p.file.SetReadDeadline(time.Now().Add(time.Duration(timeout) * time.Millisecond))
90- } else {
91- err = p.file.SetReadDeadline(time.Time{})
168+ p.added, p.ctlErr = 0, nil
169+ if transient {
170+ err := p.raw.Control(p.addFn)
171+ defer p.unwatch()
172+ if err != nil {
173+ return 0, err
174+ }
175+ if p.ctlErr != nil {
176+ return 0, p.ctlErr
177+ }
178+ }
179+ var err error
180+ switch {
181+ case timeout > 0:
182+ err, p.noDeadline = p.file.SetReadDeadline(time.Now().Add(time.Duration(timeout)*time.Millisecond)), false
183+ case !p.noDeadline:
184+ err, p.noDeadline = p.file.SetReadDeadline(time.Time{}), true
92185 }
93186 if err != nil {
94187 return 0, err
95188 }
96- var n int
97- var pollErr error
98- err = p.raw.Read(func(_ uintptr) bool {
99- n, pollErr = pollNow(fds)
100- return n != 0 || pollErr != nil
101- })
189+ err = p.raw.Read(p.readFn)
102190 if errors.Is(err, os.ErrDeadlineExceeded) {
103191 return 0, nil
104192 }
105193 if err != nil {
106194 return 0, err
107195 }
108- return n, pollErr
196+ return p.n, p.pollErr
109197 }
110198
111-// unwatch removes the registrations Poll added for fds. A failure means a
112-// watched fd was closed during the wait: its registration may outlive it.
113-func (p *Poller) unwatch(fds []unix.PollFd) {
114- p.raw.Control(func(ep uintptr) {
115- for i, f := range fds {
116- if f.Fd < 0 || listedBefore(fds, i) {
117- continue
199+// ready checks the caller's fds, the wake fd and unlisted pinned fds without
200+// blocking, retrying an interrupted check. It reports whether Poll returns.
201+func (p *Poller) ready(uintptr) bool {
202+ p.check = append(append(p.check[:0], p.fds...), unix.PollFd{Fd: p.wakeFD, Events: unix.POLLIN})
203+ for _, fd := range p.pinned {
204+ if !slices.ContainsFunc(p.fds, func(f unix.PollFd) bool { return f.Fd == fd }) {
205+ p.check = append(p.check, unix.PollFd{Fd: fd, Events: unix.POLLIN})
206+ }
207+ }
208+ // Under the wake file's reference, so a concurrent Close cannot free
209+ // its number for reuse during the check.
210+ if p.wakeRaw.Control(p.checkFn) != nil {
211+ p.n, p.pollErr = 0, os.ErrClosed
212+ }
213+ if p.pollErr != nil {
214+ p.n = 0
215+ return true
216+ }
217+ for i := range p.fds {
218+ p.fds[i].Revents = p.check[i].Revents
219+ }
220+ for _, f := range p.check[len(p.fds)+1:] {
221+ if f.Revents != 0 {
222+ // Its readiness keeps the epoll fd readable: waiting would spin.
223+ p.n, p.pollErr = 0, fmt.Errorf("fdwait: pinned fd %d is ready but not polled", f.Fd)
224+ return true
225+ }
226+ }
227+ woke := p.check[len(p.fds)].Revents != 0
228+ if woke {
229+ p.n--
230+ // A stopped poller stays awake: every later Poll returns at once.
231+ if !p.stopped() {
232+ _ = p.wakeRaw.Control(p.drainFn)
233+ if p.stopped() { // the stop's write may have been drained
234+ p.Wake()
118235 }
119- if e := unix.EpollCtl(int(ep), unix.EPOLL_CTL_DEL, int(f.Fd), nil); e != nil && p.broken == nil {
120- p.broken = fmt.Errorf("fdwait unwatch fd %d (closed during a wait?): %w", f.Fd, e)
236+ }
237+ }
238+ return p.n != 0 || woke
239+}
240+
241+func (p *Poller) pollNow(uintptr) {
242+ for {
243+ p.n, p.pollErr = unix.Poll(p.check, 0)
244+ if !errors.Is(p.pollErr, unix.EINTR) {
245+ return
246+ }
247+ }
248+}
249+
250+// skip reports whether fds[i] needs no registration of its own.
251+func (p *Poller) skip(i int) bool {
252+ fd := p.fds[i].Fd
253+ return fd < 0 || slices.Contains(p.pinned, fd) || listedBefore(p.fds, i)
254+}
255+
256+func (p *Poller) watch(ep uintptr) {
257+ for i, f := range p.fds {
258+ if !p.skip(i) {
259+ ev := unix.EpollEvent{Events: unix.EPOLLIN, Fd: f.Fd}
260+ if e := unix.EpollCtl(int(ep), unix.EPOLL_CTL_ADD, int(f.Fd), &ev); e != nil {
261+ p.ctlErr = fmt.Errorf("fdwait watch fd %d: %w", f.Fd, e)
262+ return
121263 }
122264 }
123- })
265+ p.added++
266+ }
124267 }
125268
126-// listedBefore reports whether fds[i].Fd also appears earlier in fds.
127-func listedBefore(fds []unix.PollFd, i int) bool {
128- fd := fds[i].Fd
129- return slices.ContainsFunc(fds[:i], func(f unix.PollFd) bool { return f.Fd == fd })
269+// unwatch removes the registrations Poll added. A failure means a watched
270+// fd was closed during the wait: its registration may outlive it.
271+func (p *Poller) unwatch() {
272+ if p.added > 0 {
273+ _ = p.raw.Control(p.delFn)
274+ }
130275 }
131276
132-// pollNow checks fds without blocking, retrying an interrupted check.
133-func pollNow(fds []unix.PollFd) (int, error) {
134- for {
135- n, err := unix.Poll(fds, 0)
136- if !errors.Is(err, unix.EINTR) {
137- return n, err
277+func (p *Poller) unwatchAll(ep uintptr) {
278+ for i, f := range p.fds[:p.added] {
279+ if p.skip(i) {
280+ continue
281+ }
282+ if e := unix.EpollCtl(int(ep), unix.EPOLL_CTL_DEL, int(f.Fd), nil); e != nil && p.broken == nil {
283+ p.broken = fmt.Errorf("fdwait unwatch fd %d (closed during a wait?): %w", f.Fd, e)
138284 }
139285 }
140286 }
287+
288+// listedBefore reports whether fds[i].Fd also appears earlier in fds.
289+func listedBefore(fds []unix.PollFd, i int) bool {
290+ fd := fds[i].Fd
291+ return slices.ContainsFunc(fds[:i], func(f unix.PollFd) bool { return f.Fd == fd })
292+}
@@ -2,6 +2,7 @@
2 package fdwait2 package fdwait
3 3
4 import (4 import (
5+ "context"
5 "errors"6 "errors"
6 "fmt"7 "fmt"
7 "os"8 "os"
@@ -12,16 +13,46 @@ import (
12 "golang.org/x/sys/unix"13 "golang.org/x/sys/unix"
13 )14 )
14 15
15-// Poller owns an epoll descriptor and is used by one goroutine; Close may16+// Poller owns an epoll descriptor and is used by one goroutine; Wake and
16-// also run from another goroutine to end a wait. Watched descriptors remain17+// Close may run from any goroutine. Watched descriptors remain owned by the
17-// owned by the caller, who must not close them while a Poll waits.18+// caller, who must not close them while a Poll waits.
19+//
20+// Descriptors that live as long as the poller are registered once with Pin;
21+// others are registered for one Poll call. A Poll returns on a watched fd,
22+// on Wake, once the context given to StopOn ends, or on Close: a loop needs
23+// no polling timeout to notice work or shutdown.
18 type Poller struct {24 type Poller struct {
19 file *os.File25 file *os.File
20 raw syscall.RawConn26 raw syscall.RawConn
27+ // wake is an eventfd pinned in the epoll set: Wake writes it.
28+ wake *os.File
29+ wakeRaw syscall.RawConn
30+ wakeFD int32
31+ ctx context.Context
32+ stop func() bool
33+ pinned []int32 // besides wakeFD
21 // broken is set when a registration could not be removed: it may keep34 // broken is set when a registration could not be removed: it may keep
22 // the epoll descriptor readable forever, so every later Poll fails35 // the epoll descriptor readable forever, so every later Poll fails
23 // instead of spinning.36 // instead of spinning.
24 broken error37 broken error
38+ // noDeadline: the read deadline is already cleared.
39+ noDeadline bool
40+
41+ // State of the running Poll, used by the callbacks below. They are
42+ // bound once in New, so a Poll allocates nothing.
43+ fds []unix.PollFd
44+ check []unix.PollFd
45+ added int
46+ ctlErr error
47+ n int
48+ pollErr error
49+ buf [8]byte
50+ addFn func(uintptr)
51+ delFn func(uintptr)
52+ checkFn func(uintptr)
53+ readFn func(uintptr) bool
54+ drainFn func(uintptr)
55+ kickFn func(uintptr)
25 }56 }
26 57
27 func New() (*Poller, error) {58 func New() (*Poller, error) {
@@ -33,108 +64,229 @@ func New() (*Poller, error) {
33 unix.Close(fd)64 unix.Close(fd)
34 return nil, fmt.Errorf("fdwait epoll nonblock: %w", err)65 return nil, fmt.Errorf("fdwait epoll nonblock: %w", err)
35 }66 }
36- f := os.NewFile(uintptr(fd), "fdwait")67+ p := &Poller{file: os.NewFile(uintptr(fd), "fdwait")}
37- raw, err := f.SyscallConn()68+ if p.raw, err = p.file.SyscallConn(); err != nil {
38- if err != nil {69+ p.file.Close()
39- f.Close()
40 return nil, fmt.Errorf("fdwait epoll conn: %w", err)70 return nil, fmt.Errorf("fdwait epoll conn: %w", err)
41 }71 }
42- return &Poller{file: f, raw: raw}, nil72+ efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
73+ if err != nil {
74+ p.file.Close()
75+ return nil, fmt.Errorf("fdwait wake eventfd: %w", err)
76+ }
77+ p.wake, p.wakeFD = os.NewFile(uintptr(efd), "fdwait-wake"), int32(efd)
78+ p.addFn, p.delFn, p.readFn, p.checkFn = p.watch, p.unwatchAll, p.ready, p.pollNow
79+ p.drainFn, p.kickFn = p.drain, p.kick
80+ if p.wakeRaw, err = p.wake.SyscallConn(); err == nil {
81+ err = p.control(unix.EPOLL_CTL_ADD, p.wakeFD)
82+ }
83+ if err != nil {
84+ p.Close()
85+ return nil, fmt.Errorf("fdwait wake: %w", err)
86+ }
87+ return p, nil
43 }88 }
44 89
45-func (p *Poller) Close() error { return p.file.Close() }90+// Close ends a running Poll and releases the epoll and wake descriptors.
91+func (p *Poller) Close() error {
92+ if p.stop != nil {
93+ p.stop()
94+ }
95+ err := p.file.Close()
96+ p.wake.Close()
97+ return err
98+}
99+
100+func (p *Poller) control(op int, fd int32) error {
101+ var err error
102+ if cerr := p.raw.Control(func(ep uintptr) {
103+ ev := unix.EpollEvent{Events: unix.EPOLLIN, Fd: fd}
104+ err = unix.EpollCtl(int(ep), op, int(fd), &ev)
105+ }); cerr != nil {
106+ return cerr
107+ }
108+ return err
109+}
110+
111+// Pin registers fd for every later Poll, which then costs it no epoll call.
112+// fd must stay open until Close, and every Poll must list it: a ready pinned
113+// fd that a Poll does not list fails that Poll.
114+func (p *Poller) Pin(fd int32) error {
115+ if err := p.control(unix.EPOLL_CTL_ADD, fd); err != nil {
116+ return fmt.Errorf("fdwait pin fd %d: %w", fd, err)
117+ }
118+ p.pinned = append(p.pinned, fd)
119+ return nil
120+}
121+
122+// StopOn makes every Poll return (0, nil) at once after ctx ends. Call it
123+// once, before polling.
124+func (p *Poller) StopOn(ctx context.Context) {
125+ p.ctx = ctx
126+ p.stop = context.AfterFunc(ctx, p.Wake)
127+}
128+
129+// Wake makes the running or next Poll return. Wakes before that Poll
130+// coalesce. Safe from any goroutine, also after Close.
131+func (p *Poller) Wake() { _ = p.wakeRaw.Control(p.kickFn) }
132+
133+func (p *Poller) kick(fd uintptr) {
134+ one := [8]byte{1}
135+ _, _ = unix.Write(int(fd), one[:])
136+}
137+
138+func (p *Poller) drain(fd uintptr) { _, _ = unix.Read(int(fd), p.buf[:]) }
139+
140+func (p *Poller) stopped() bool { return p.ctx != nil && p.ctx.Err() != nil }
46 141
47 // Poll waits like poll(2) for readable/error events on fds and fills their142 // Poll waits like poll(2) for readable/error events on fds and fills their
48 // Revents. It never consumes caller events. Negative fds are ignored and a143 // Revents. It never consumes caller events. Negative fds are ignored and a
49 // descriptor listed twice is watched once. timeout is in milliseconds; a144 // descriptor listed twice is watched once. timeout is in milliseconds; a
50-// negative one waits until an event or Close.145+// negative one waits until an event, Wake, the end of the StopOn context or
146+// Close. A Wake or stop returns the events seen, possibly none.
51 //147 //
52-// Each call explicitly removes its epoll registrations before returning.148+// Each call removes the registrations it made before returning. Relying on
53-// Relying on close to remove them is unsafe: another fd or a kernel fence149+// close to remove them is unsafe: another fd or a kernel fence may retain
54-// may retain the open file description and keep the epoll fd readable.150+// the open file description and keep the epoll fd readable. When nothing is
55-// When nothing is ready yet, a descriptor epoll cannot watch (EPERM,151+// ready yet, a descriptor epoll cannot watch (EPERM, EINVAL, ELOOP for epoll
56-// EINVAL, ELOOP for epoll nested too deep) fails the call.152+// nested too deep) fails the call.
57 func (p *Poller) Poll(fds []unix.PollFd, timeout int) (int, error) {153 func (p *Poller) Poll(fds []unix.PollFd, timeout int) (int, error) {
58 if p.broken != nil {154 if p.broken != nil {
59 return 0, p.broken155 return 0, p.broken
60 }156 }
61- // Ready descriptors need no registration, and an expired deadline157+ p.fds = fds
62- // would skip the readiness check in RawConn.Read.158+ defer func() { p.fds = nil }()
63- if n, err := pollNow(fds); n != 0 || err != nil || timeout == 0 {159+ transient := false
64- return n, err160+ for i := range fds {
65- }161+ transient = transient || !p.skip(i)
66- var ctlErr error
67- added := 0
68- err := p.raw.Control(func(ep uintptr) {
69- for _, f := range fds {
70- if f.Fd >= 0 {
71- ev := unix.EpollEvent{Events: unix.EPOLLIN, Fd: f.Fd}
72- e := unix.EpollCtl(int(ep), unix.EPOLL_CTL_ADD, int(f.Fd), &ev)
73- if e != nil && !(errors.Is(e, unix.EEXIST) && listedBefore(fds, added)) {
74- ctlErr = fmt.Errorf("fdwait watch fd %d: %w", f.Fd, e)
75- return
76- }
77- }
78- added++
79- }
80- })
81- defer p.unwatch(fds[:added])
82- if err != nil {
83- return 0, err
84 }162 }
85- if ctlErr != nil {163+ // An expired deadline skips RawConn.Read's own check, and a ready
86- return 0, ctlErr164+ // transient fd needs no registration. Pinned fds are checked by Read.
165+ if (timeout == 0 || transient) && p.ready(0) || timeout == 0 {
166+ return p.n, p.pollErr
87 }167 }
88- if timeout > 0 {168+ p.added, p.ctlErr = 0, nil
89- err = p.file.SetReadDeadline(time.Now().Add(time.Duration(timeout) * time.Millisecond))169+ if transient {
90- } else {170+ err := p.raw.Control(p.addFn)
91- err = p.file.SetReadDeadline(time.Time{})171+ defer p.unwatch()
172+ if err != nil {
173+ return 0, err
174+ }
175+ if p.ctlErr != nil {
176+ return 0, p.ctlErr
177+ }
178+ }
179+ var err error
180+ switch {
181+ case timeout > 0:
182+ err, p.noDeadline = p.file.SetReadDeadline(time.Now().Add(time.Duration(timeout)*time.Millisecond)), false
183+ case !p.noDeadline:
184+ err, p.noDeadline = p.file.SetReadDeadline(time.Time{}), true
92 }185 }
93 if err != nil {186 if err != nil {
94 return 0, err187 return 0, err
95 }188 }
96- var n int189+ err = p.raw.Read(p.readFn)
97- var pollErr error
98- err = p.raw.Read(func(_ uintptr) bool {
99- n, pollErr = pollNow(fds)
100- return n != 0 || pollErr != nil
101- })
102 if errors.Is(err, os.ErrDeadlineExceeded) {190 if errors.Is(err, os.ErrDeadlineExceeded) {
103 return 0, nil191 return 0, nil
104 }192 }
105 if err != nil {193 if err != nil {
106 return 0, err194 return 0, err
107 }195 }
108- return n, pollErr196+ return p.n, p.pollErr
109 }197 }
110 198
111-// unwatch removes the registrations Poll added for fds. A failure means a199+// ready checks the caller's fds, the wake fd and unlisted pinned fds without
112-// watched fd was closed during the wait: its registration may outlive it.200+// blocking, retrying an interrupted check. It reports whether Poll returns.
113-func (p *Poller) unwatch(fds []unix.PollFd) {201+func (p *Poller) ready(uintptr) bool {
114- p.raw.Control(func(ep uintptr) {202+ p.check = append(append(p.check[:0], p.fds...), unix.PollFd{Fd: p.wakeFD, Events: unix.POLLIN})
115- for i, f := range fds {203+ for _, fd := range p.pinned {
116- if f.Fd < 0 || listedBefore(fds, i) {204+ if !slices.ContainsFunc(p.fds, func(f unix.PollFd) bool { return f.Fd == fd }) {
117- continue205+ p.check = append(p.check, unix.PollFd{Fd: fd, Events: unix.POLLIN})
206+ }
207+ }
208+ // Under the wake file's reference, so a concurrent Close cannot free
209+ // its number for reuse during the check.
210+ if p.wakeRaw.Control(p.checkFn) != nil {
211+ p.n, p.pollErr = 0, os.ErrClosed
212+ }
213+ if p.pollErr != nil {
214+ p.n = 0
215+ return true
216+ }
217+ for i := range p.fds {
218+ p.fds[i].Revents = p.check[i].Revents
219+ }
220+ for _, f := range p.check[len(p.fds)+1:] {
221+ if f.Revents != 0 {
222+ // Its readiness keeps the epoll fd readable: waiting would spin.
223+ p.n, p.pollErr = 0, fmt.Errorf("fdwait: pinned fd %d is ready but not polled", f.Fd)
224+ return true
225+ }
226+ }
227+ woke := p.check[len(p.fds)].Revents != 0
228+ if woke {
229+ p.n--
230+ // A stopped poller stays awake: every later Poll returns at once.
231+ if !p.stopped() {
232+ _ = p.wakeRaw.Control(p.drainFn)
233+ if p.stopped() { // the stop's write may have been drained
234+ p.Wake()
118 }235 }
119- if e := unix.EpollCtl(int(ep), unix.EPOLL_CTL_DEL, int(f.Fd), nil); e != nil && p.broken == nil {236+ }
120- p.broken = fmt.Errorf("fdwait unwatch fd %d (closed during a wait?): %w", f.Fd, e)237+ }
238+ return p.n != 0 || woke
239+}
240+
241+func (p *Poller) pollNow(uintptr) {
242+ for {
243+ p.n, p.pollErr = unix.Poll(p.check, 0)
244+ if !errors.Is(p.pollErr, unix.EINTR) {
245+ return
246+ }
247+ }
248+}
249+
250+// skip reports whether fds[i] needs no registration of its own.
251+func (p *Poller) skip(i int) bool {
252+ fd := p.fds[i].Fd
253+ return fd < 0 || slices.Contains(p.pinned, fd) || listedBefore(p.fds, i)
254+}
255+
256+func (p *Poller) watch(ep uintptr) {
257+ for i, f := range p.fds {
258+ if !p.skip(i) {
259+ ev := unix.EpollEvent{Events: unix.EPOLLIN, Fd: f.Fd}
260+ if e := unix.EpollCtl(int(ep), unix.EPOLL_CTL_ADD, int(f.Fd), &ev); e != nil {
261+ p.ctlErr = fmt.Errorf("fdwait watch fd %d: %w", f.Fd, e)
262+ return
121 }263 }
122 }264 }
123- })265+ p.added++
266+ }
124 }267 }
125 268
126-// listedBefore reports whether fds[i].Fd also appears earlier in fds.269+// unwatch removes the registrations Poll added. A failure means a watched
127-func listedBefore(fds []unix.PollFd, i int) bool {270+// fd was closed during the wait: its registration may outlive it.
128- fd := fds[i].Fd271+func (p *Poller) unwatch() {
129- return slices.ContainsFunc(fds[:i], func(f unix.PollFd) bool { return f.Fd == fd })272+ if p.added > 0 {
273+ _ = p.raw.Control(p.delFn)
274+ }
130 }275 }
131 276
132-// pollNow checks fds without blocking, retrying an interrupted check.277+func (p *Poller) unwatchAll(ep uintptr) {
133-func pollNow(fds []unix.PollFd) (int, error) {278+ for i, f := range p.fds[:p.added] {
134- for {279+ if p.skip(i) {
135- n, err := unix.Poll(fds, 0)280+ continue
136- if !errors.Is(err, unix.EINTR) {281+ }
137- return n, err282+ if e := unix.EpollCtl(int(ep), unix.EPOLL_CTL_DEL, int(f.Fd), nil); e != nil && p.broken == nil {
283+ p.broken = fmt.Errorf("fdwait unwatch fd %d (closed during a wait?): %w", f.Fd, e)
138 }284 }
139 }285 }
140 }286 }
287+
288+// listedBefore reports whether fds[i].Fd also appears earlier in fds.
289+func listedBefore(fds []unix.PollFd, i int) bool {
290+ fd := fds[i].Fd
291+ return slices.ContainsFunc(fds[:i], func(f unix.PollFd) bool { return f.Fd == fd })
292+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 26

Tags 10

↑↓ moveenter openesc close