Skip to content

Commit 7ad867a

Browse files
committed
xio: Add Reader/Writer/... interfaces - io analogs that add support for contexts
In many cases IO needs to be able to be canceled. For example in WCFS filesystem I need to cancel handling sysread(/head/watch) when FUSE INTERRUPT request comes in [1,2,3]. The READ handler for /head/watch inside WCFS is interally implemented via io.Pipe which does not provide read/write cancellattion besides "destructive" close. Standard Go answer for cancellations is via contexts. So as a first step let's add corresponding interfaces - xio.Reader, xio.Writer etc - that are io analogs that add support for contexts. For compatibility with legacy code that work with only io.X (e.g. only with io.Reader), in spirit of [4], add BindCtx which binds xio.X instance with context and converts it into io.X. Add WithCtx - corresponding inverse operation that converts io.X back into xio.X and for general io.X adds cancellation handling on a best-effort basis. [1] https://lab.nexedi.com/kirr/wendelin.core/commit/b17aeb8c [2] https://lab.nexedi.com/kirr/wendelin.core/commit/f05271b1 [3] https://lab.nexedi.com/kirr/wendelin.core/commit/5ba816da [4] golang/go#20280 [5] golang/go#16522
1 parent 5f6ae15 commit 7ad867a

File tree

2 files changed

+372
-1
lines changed

2 files changed

+372
-1
lines changed

xio/xio.go

Lines changed: 251 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,259 @@
1818
// See https://www.nexedi.com/licensing for rationale and options.
1919

2020
// Package xio provides addons to standard package io.
21+
//
22+
// - Reader, Writer, ReadWriter, etc are io analogs that add support for contexts.
23+
// - BindCtx*(X, ctx) converts xio.X into io.X that implicitly passes ctx
24+
// to xio.X and can be used in legacy code.
25+
// - WithCtx*(X) converts io.X back into xio.X that accepts context.
26+
// It is the opposite operation for BindCtx, but for arbitrary io.X
27+
// returned xio.X handles context only on best-effort basis. In
28+
// particular IO cancellation is not reliably handled for os.File .
2129
package xio
2230

23-
import "io"
31+
import (
32+
"context"
33+
"io"
34+
)
35+
36+
// Reader is like io.Reader but additionally takes context for Read.
37+
type Reader interface {
38+
Read(ctx context.Context, dst []byte) (n int, err error)
39+
}
40+
41+
// Writer is like io.Writer but additionally takes context for Write.
42+
type Writer interface {
43+
Write(ctx context.Context, src []byte) (n int, err error)
44+
}
45+
46+
// ReadWriter combines Reader and Writer.
47+
type ReadWriter interface {
48+
Reader
49+
Writer
50+
}
51+
52+
// ReadCloser combines Reader and io.Closer.
53+
type ReadCloser interface {
54+
Reader
55+
io.Closer
56+
}
57+
58+
// WriteCloser combines Writer and io.Closer.
59+
type WriteCloser interface {
60+
Writer
61+
io.Closer
62+
}
63+
64+
// ReadWriteCloser combines Reader, Writer and io.Closer.
65+
type ReadWriteCloser interface {
66+
Reader
67+
Writer
68+
io.Closer
69+
}
70+
71+
72+
// BindCtx*(xio.X, ctx) -> io.X
73+
//
74+
// XXX better just BindCtx(x T, ctx) -> T with all x IO methods without ctx,
75+
// but that needs either generics, or support from reflect to preserve optional
76+
// methods: https://github.com/golang/go/issues/16522.
77+
78+
79+
// BindCtxR binds Reader r and ctx into io.Reader which passes ctx to r on every Read.
80+
func BindCtxR(r Reader, ctx context.Context) io.Reader {
81+
// BindCtx(WithCtx(X), BG) = X
82+
if ctx.Done() == nil {
83+
switch s := r.(type) {
84+
case *stubCtxR: return s.r
85+
case *stubCtxRW: return s.rw
86+
case *stubCtxRC: return s.r
87+
case *stubCtxRWC: return s.rw
88+
}
89+
}
90+
91+
return &bindCtxR{r, ctx}
92+
}
93+
type bindCtxR struct {r Reader; ctx context.Context}
94+
func (b *bindCtxR) Read(dst []byte) (int, error) { return b.r.Read(b.ctx, dst) }
95+
96+
// BindCtxW binds Writer w and ctx into io.Writer which passes ctx to w on every Write.
97+
func BindCtxW(w Writer, ctx context.Context) io.Writer {
98+
if ctx.Done() == nil {
99+
switch s := w.(type) {
100+
case *stubCtxW: return s.w
101+
case *stubCtxRW: return s.rw
102+
case *stubCtxWC: return s.w
103+
case *stubCtxRWC: return s.rw
104+
}
105+
}
106+
return &bindCtxW{w, ctx}
107+
}
108+
type bindCtxW struct {w Writer; ctx context.Context}
109+
func (b *bindCtxW) Write(src []byte) (int, error) { return b.w.Write(b.ctx, src) }
110+
111+
// BindCtxRW binds ReadWriter rw and ctx into io.ReadWriter which passes ctx to
112+
// rw on every Read and Write.
113+
func BindCtxRW(rw ReadWriter, ctx context.Context) io.ReadWriter {
114+
if ctx.Done() == nil {
115+
switch s := rw.(type) {
116+
case *stubCtxRW: return s.rw
117+
case *stubCtxRWC: return s.rw
118+
}
119+
}
120+
return &bindCtxRW{rw, ctx}
121+
}
122+
type bindCtxRW struct {rw ReadWriter; ctx context.Context}
123+
func (b *bindCtxRW) Read (dst []byte) (int, error) { return b.rw.Read (b.ctx, dst) }
124+
func (b *bindCtxRW) Write(src []byte) (int, error) { return b.rw.Write(b.ctx, src) }
125+
126+
// BindCtxRC binds ReadCloser r and ctx into io.ReadCloser which passes ctx to r on every Read.
127+
func BindCtxRC(r ReadCloser, ctx context.Context) io.ReadCloser {
128+
if ctx.Done() == nil {
129+
switch s := r.(type) {
130+
case *stubCtxRC: return s.r
131+
case *stubCtxRWC: return s.rw
132+
}
133+
}
134+
return &bindCtxRC{r, ctx}
135+
}
136+
type bindCtxRC struct {r ReadCloser; ctx context.Context}
137+
func (b *bindCtxRC) Read(dst []byte) (int, error) { return b.r.Read(b.ctx, dst) }
138+
func (b *bindCtxRC) Close() error { return b.r.Close() }
139+
140+
// BindCtxWC binds WriteCloser w and ctx into io.WriteCloser which passes ctx to w on every Write.
141+
func BindCtxWC(w WriteCloser, ctx context.Context) io.WriteCloser {
142+
if ctx.Done() == nil {
143+
switch s := w.(type) {
144+
case *stubCtxWC: return s.w
145+
case *stubCtxRWC: return s.rw
146+
}
147+
}
148+
return &bindCtxWC{w, ctx}
149+
}
150+
type bindCtxWC struct {w WriteCloser; ctx context.Context}
151+
func (b *bindCtxWC) Write(src []byte) (int, error) { return b.w.Write(b.ctx, src) }
152+
func (b *bindCtxWC) Close() error { return b.w.Close() }
153+
154+
// BindCtxRWC binds ReadWriteCloser rw and ctx into io.ReadWriteCloser
155+
// which passes ctx to rw on every Read and Write.
156+
func BindCtxRWC(rw ReadWriteCloser, ctx context.Context) io.ReadWriteCloser {
157+
if ctx.Done() == nil {
158+
switch s := rw.(type) {
159+
case *stubCtxRWC: return s.rw
160+
}
161+
}
162+
return &bindCtxRWC{rw, ctx}
163+
}
164+
type bindCtxRWC struct {rw ReadWriteCloser; ctx context.Context}
165+
func (b *bindCtxRWC) Read(dst []byte) (int, error) { return b.rw.Read(b.ctx, dst) }
166+
func (b *bindCtxRWC) Write(src []byte) (int, error) { return b.rw.Write(b.ctx, src) }
167+
func (b *bindCtxRWC) Close() error { return b.rw.Close() }
168+
169+
170+
// WithCtx*(io.X) -> xio.X that handles ctx on best-effort basis.
171+
//
172+
// FIXME for arbitrary io.X for now ctx is completely ignored.
173+
// TODO add support for cancellation if io.X provides working .Set{Read/Write}Deadline:
174+
// https://medium.com/@zombiezen/canceling-i-o-in-go-capn-proto-5ae8c09c5b29
175+
// https://github.com/golang/go/issues/20280
176+
177+
// WithCtxR converts io.Reader r into Reader that accepts ctx.
178+
//
179+
// It returns original IO object if r was created via BindCtx*, but in general
180+
// returned Reader will handle context only on best-effort basis.
181+
func WithCtxR(r io.Reader) Reader {
182+
// WithCtx(BindCtx(X)) = X
183+
switch b := r.(type) {
184+
case *bindCtxR: return b.r
185+
case *bindCtxRW: return b.rw
186+
case *bindCtxRC: return b.r
187+
case *bindCtxRWC: return b.rw
188+
}
189+
190+
return &stubCtxR{r}
191+
}
192+
type stubCtxR struct {r io.Reader}
193+
func (s *stubCtxR) Read(ctx context.Context, dst []byte) (int, error) { return s.r.Read(dst) }
194+
195+
// WithCtxW converts io.Writer w into Writer that accepts ctx.
196+
//
197+
// It returns original IO object if w was created via BindCtx*, but in general
198+
// returned Writer will handle context only on best-effort basis.
199+
func WithCtxW(w io.Writer) Writer {
200+
switch b := w.(type) {
201+
case *bindCtxW: return b.w
202+
case *bindCtxRW: return b.rw
203+
case *bindCtxWC: return b.w
204+
case *bindCtxRWC: return b.rw
205+
}
206+
return &stubCtxW{w}
207+
}
208+
type stubCtxW struct {w io.Writer}
209+
func (s *stubCtxW) Write(ctx context.Context, src []byte) (int, error) { return s.w.Write(src) }
210+
211+
// WithCtxRW converts io.ReadWriter rw into ReadWriter that accepts ctx.
212+
//
213+
// It returns original IO object if rw was created via BindCtx*, but in general
214+
// returned ReadWriter will handle context only on best-effort basis.
215+
func WithCtxRW(rw io.ReadWriter) ReadWriter {
216+
switch b := rw.(type) {
217+
case *bindCtxRW: return b.rw
218+
case *bindCtxRWC: return b.rw
219+
}
220+
return &stubCtxRW{rw}
221+
}
222+
type stubCtxRW struct {rw io.ReadWriter}
223+
func (s *stubCtxRW) Read (ctx context.Context, dst []byte) (int, error) { return s.rw.Read (dst) }
224+
func (s *stubCtxRW) Write(ctx context.Context, src []byte) (int, error) { return s.rw.Write(src) }
225+
226+
// WithCtxRC converts io.ReadCloser r into ReadCloser that accepts ctx.
227+
//
228+
// It returns original IO object if r was created via BindCtx*, but in general
229+
// returned ReadCloser will handle context only on best-effort basis.
230+
func WithCtxRC(r io.ReadCloser) ReadCloser {
231+
switch b := r.(type) {
232+
case *bindCtxRC: return b.r
233+
case *bindCtxRWC: return b.rw
234+
}
235+
return &stubCtxRC{r}
236+
}
237+
type stubCtxRC struct {r io.ReadCloser}
238+
func (s *stubCtxRC) Read (ctx context.Context, dst []byte) (int, error) { return s.r.Read(dst) }
239+
func (s *stubCtxRC) Close() error { return s.r.Close() }
240+
241+
// WithCtxWC converts io.WriteCloser w into WriteCloser that accepts ctx.
242+
//
243+
// It returns original IO object if w was created via BindCtx*, but in general
244+
// returned WriteCloser will handle context only on best-effort basis.
245+
func WithCtxWC(w io.WriteCloser) WriteCloser {
246+
switch b := w.(type) {
247+
case *bindCtxWC: return b.w
248+
case *bindCtxRWC: return b.rw
249+
}
250+
return &stubCtxWC{w}
251+
}
252+
type stubCtxWC struct {w io.WriteCloser}
253+
func (s *stubCtxWC) Write(ctx context.Context, src []byte) (int, error) { return s.w.Write(src) }
254+
func (s *stubCtxWC) Close() error { return s.w.Close() }
255+
256+
// WithCtxRWC converts io.ReadWriteCloser rw into ReadWriteCloser that accepts ctx.
257+
//
258+
// It returns original IO object if rw was created via BindCtx*, but in general
259+
// returned ReadWriteCloser will handle context only on best-effort basis.
260+
func WithCtxRWC(rw io.ReadWriteCloser) ReadWriteCloser {
261+
switch b := rw.(type) {
262+
case *bindCtxRWC: return b.rw
263+
}
264+
return &stubCtxRWC{rw}
265+
}
266+
type stubCtxRWC struct {rw io.ReadWriteCloser}
267+
func (s *stubCtxRWC) Read (ctx context.Context, dst []byte) (int, error){ return s.rw.Read (dst) }
268+
func (s *stubCtxRWC) Write(ctx context.Context, src []byte) (int, error){ return s.rw.Write(src) }
269+
func (s *stubCtxRWC) Close() error { return s.rw.Close() }
270+
271+
272+
// ----------------------------------------
273+
24274

25275
// CountedReader is an io.Reader that count total bytes read.
26276
type CountedReader struct {

xio/xio_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright (C) 2019 Nexedi SA and Contributors.
2+
// Kirill Smelkov <[email protected]>
3+
//
4+
// This program is free software: you can Use, Study, Modify and Redistribute
5+
// it under the terms of the GNU General Public License version 3, or (at your
6+
// option) any later version, as published by the Free Software Foundation.
7+
//
8+
// You can also Link and Combine this program with other software covered by
9+
// the terms of any of the Free Software licenses or any of the Open Source
10+
// Initiative approved licenses and Convey the resulting work. Corresponding
11+
// source of such a combination shall include the source code for all other
12+
// software used.
13+
//
14+
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
15+
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16+
//
17+
// See COPYING file for full licensing terms.
18+
// See https://www.nexedi.com/licensing for rationale and options.
19+
20+
package xio
21+
22+
import (
23+
"context"
24+
"testing"
25+
)
26+
27+
// xIO is test Reader/Writer/Closer/...
28+
type xIO struct{}
29+
30+
func (_ *xIO) Read(ctx context.Context, dst []byte) (int, error) {
31+
for i := range dst {
32+
dst[i] = 0
33+
}
34+
return len(dst), nil
35+
}
36+
37+
func (_ *xIO) Write(ctx context.Context, src []byte) (int, error) {
38+
return len(src), nil
39+
}
40+
41+
func (_ *xIO) Close() error {
42+
return nil
43+
}
44+
45+
// tIO is test io.Reader/io.Writer/...
46+
type tIO struct{}
47+
48+
func (_ *tIO) Read(dst []byte) (int, error) {
49+
for i := range dst {
50+
dst[i] = 0
51+
}
52+
return len(dst), nil
53+
}
54+
55+
func (_ *tIO) Write(src []byte) (int, error) {
56+
return len(src), nil
57+
}
58+
59+
func (_ *tIO) Close() error {
60+
return nil
61+
}
62+
63+
64+
// ok1 asserts that v is true.
65+
func ok1(v bool) {
66+
if !v {
67+
panic("not ok")
68+
}
69+
}
70+
71+
// Verify xio.X <-> io.X conversion
72+
func TestConvert(t *testing.T) {
73+
x := new(xIO)
74+
i := new(tIO)
75+
bg := context.Background()
76+
77+
// WithCtx(BindCtx(X)) = X
78+
ok1( WithCtxR(BindCtxR(x, bg)) == x )
79+
80+
ok1( WithCtxW(BindCtxW(x, bg)) == x )
81+
82+
ok1( WithCtxR (BindCtxRW(x, bg)) == x )
83+
ok1( WithCtxW (BindCtxRW(x, bg)) == x )
84+
ok1( WithCtxRW(BindCtxRW(x, bg)) == x )
85+
86+
ok1( WithCtxR (BindCtxRC(x, bg)) == x )
87+
ok1( WithCtxRC(BindCtxRC(x, bg)) == x )
88+
89+
ok1( WithCtxW (BindCtxWC(x, bg)) == x )
90+
ok1( WithCtxWC(BindCtxWC(x, bg)) == x )
91+
92+
ok1( WithCtxR (BindCtxRWC(x, bg)) == x )
93+
ok1( WithCtxW (BindCtxRWC(x, bg)) == x )
94+
ok1( WithCtxRW (BindCtxRWC(x, bg)) == x )
95+
ok1( WithCtxRC (BindCtxRWC(x, bg)) == x )
96+
ok1( WithCtxWC (BindCtxRWC(x, bg)) == x )
97+
ok1( WithCtxRWC(BindCtxRWC(x, bg)) == x )
98+
99+
100+
// BindCtx(WithCtx(X), bg) = X
101+
ok1( BindCtxR(WithCtxR(i), bg) == i )
102+
103+
ok1( BindCtxW(WithCtxW(i), bg) == i )
104+
105+
ok1( BindCtxR (WithCtxRW(i), bg) == i )
106+
ok1( BindCtxW (WithCtxRW(i), bg) == i )
107+
ok1( BindCtxRW(WithCtxRW(i), bg) == i )
108+
109+
ok1( BindCtxR (WithCtxRC(i), bg) == i )
110+
ok1( BindCtxRC(WithCtxRC(i), bg) == i )
111+
112+
ok1( BindCtxW (WithCtxWC(i), bg) == i )
113+
ok1( BindCtxWC(WithCtxWC(i), bg) == i )
114+
115+
ok1( BindCtxR (WithCtxRWC(i), bg) == i )
116+
ok1( BindCtxW (WithCtxRWC(i), bg) == i )
117+
ok1( BindCtxRW (WithCtxRWC(i), bg) == i )
118+
ok1( BindCtxRC (WithCtxRWC(i), bg) == i )
119+
ok1( BindCtxWC (WithCtxRWC(i), bg) == i )
120+
ok1( BindCtxRWC(WithCtxRWC(i), bg) == i )
121+
}

0 commit comments

Comments
 (0)