1
0
mirror of https://github.com/v2fly/v2ray-core.git synced 2025-06-02 22:03:39 -04:00

update activity timer

This commit is contained in:
Darien Raymond 2018-05-27 14:42:53 +02:00
parent 46dbbfff8c
commit ac6a0f7511
No known key found for this signature in database
GPG Key ID: 7251FFA14BB18169
20 changed files with 141 additions and 219 deletions

View File

@ -10,7 +10,7 @@ import (
"google.golang.org/grpc" "google.golang.org/grpc"
"v2ray.com/core" "v2ray.com/core"
"v2ray.com/core/common" "v2ray.com/core/common"
"v2ray.com/core/common/signal" "v2ray.com/core/common/signal/done"
) )
// Commander is a V2Ray feature that provides gRPC methods to external clients. // Commander is a V2Ray feature that provides gRPC methods to external clients.
@ -64,7 +64,7 @@ func (c *Commander) Start() error {
listener := &OutboundListener{ listener := &OutboundListener{
buffer: make(chan net.Conn, 4), buffer: make(chan net.Conn, 4),
done: signal.NewDone(), done: done.New(),
} }
go func() { go func() {

View File

@ -8,13 +8,14 @@ import (
"v2ray.com/core/common" "v2ray.com/core/common"
"v2ray.com/core/common/net" "v2ray.com/core/common/net"
"v2ray.com/core/common/signal" "v2ray.com/core/common/signal"
"v2ray.com/core/common/signal/done"
"v2ray.com/core/transport/pipe" "v2ray.com/core/transport/pipe"
) )
// OutboundListener is a net.Listener for listening gRPC connections. // OutboundListener is a net.Listener for listening gRPC connections.
type OutboundListener struct { type OutboundListener struct {
buffer chan net.Conn buffer chan net.Conn
done *signal.Done done *done.Instance
} }
func (l *OutboundListener) add(conn net.Conn) { func (l *OutboundListener) add(conn net.Conn) {

View File

@ -13,7 +13,7 @@ import (
"v2ray.com/core/common/buf" "v2ray.com/core/common/buf"
"v2ray.com/core/common/net" "v2ray.com/core/common/net"
"v2ray.com/core/common/session" "v2ray.com/core/common/session"
"v2ray.com/core/common/signal" "v2ray.com/core/common/signal/done"
"v2ray.com/core/proxy" "v2ray.com/core/proxy"
"v2ray.com/core/transport/internet" "v2ray.com/core/transport/internet"
"v2ray.com/core/transport/internet/tcp" "v2ray.com/core/transport/internet/tcp"
@ -115,7 +115,7 @@ type udpConn struct {
output func([]byte) (int, error) output func([]byte) (int, error)
remote net.Addr remote net.Addr
local net.Addr local net.Addr
done *signal.Done done *done.Instance
uplink core.StatCounter uplink core.StatCounter
downlink core.StatCounter downlink core.StatCounter
} }
@ -223,7 +223,7 @@ type udpWorker struct {
uplinkCounter core.StatCounter uplinkCounter core.StatCounter
downlinkCounter core.StatCounter downlinkCounter core.StatCounter
done *signal.Done done *done.Instance
activeConn map[connID]*udpConn activeConn map[connID]*udpConn
} }
@ -248,7 +248,7 @@ func (w *udpWorker) getConnection(id connID) (*udpConn, bool) {
IP: w.address.IP(), IP: w.address.IP(),
Port: int(w.port), Port: int(w.port),
}, },
done: signal.NewDone(), done: done.New(),
uplink: w.uplinkCounter, uplink: w.uplinkCounter,
downlink: w.downlinkCounter, downlink: w.downlinkCounter,
} }
@ -305,7 +305,7 @@ func (w *udpWorker) removeConn(id connID) {
func (w *udpWorker) Start() error { func (w *udpWorker) Start() error {
w.activeConn = make(map[connID]*udpConn, 16) w.activeConn = make(map[connID]*udpConn, 16)
w.done = signal.NewDone() w.done = done.New()
h, err := udp.ListenUDP(w.address, w.port, w.callback, udp.HubReceiveOriginalDestination(w.recvOrigDest), udp.HubCapacity(256)) h, err := udp.ListenUDP(w.address, w.port, w.callback, udp.HubReceiveOriginalDestination(w.recvOrigDest), udp.HubCapacity(256))
if err != nil { if err != nil {
return err return err

View File

@ -16,7 +16,7 @@ import (
"v2ray.com/core/common/log" "v2ray.com/core/common/log"
"v2ray.com/core/common/net" "v2ray.com/core/common/net"
"v2ray.com/core/common/protocol" "v2ray.com/core/common/protocol"
"v2ray.com/core/common/signal" "v2ray.com/core/common/signal/done"
"v2ray.com/core/proxy" "v2ray.com/core/proxy"
"v2ray.com/core/transport/pipe" "v2ray.com/core/transport/pipe"
) )
@ -77,7 +77,7 @@ func (m *ClientManager) onClientFinish() {
type Client struct { type Client struct {
sessionManager *SessionManager sessionManager *SessionManager
link core.Link link core.Link
done *signal.Done done *done.Instance
manager *ClientManager manager *ClientManager
concurrency uint32 concurrency uint32
} }
@ -100,7 +100,7 @@ func NewClient(pctx context.Context, p proxy.Outbound, dialer proxy.Dialer, m *C
Reader: downlinkReader, Reader: downlinkReader,
Writer: upLinkWriter, Writer: upLinkWriter,
}, },
done: signal.NewDone(), done: done.New(),
manager: m, manager: m,
concurrency: m.config.Concurrency, concurrency: m.config.Concurrency,
} }

View File

@ -7,7 +7,8 @@ import (
"time" "time"
"v2ray.com/core/common/platform" "v2ray.com/core/common/platform"
"v2ray.com/core/common/signal" "v2ray.com/core/common/signal/done"
"v2ray.com/core/common/signal/semaphore"
) )
// Writer is the interface for writing logs. // Writer is the interface for writing logs.
@ -22,8 +23,8 @@ type WriterCreator func() Writer
type generalLogger struct { type generalLogger struct {
creator WriterCreator creator WriterCreator
buffer chan Message buffer chan Message
access *signal.Semaphore access *semaphore.Instance
done *signal.Done done *done.Instance
} }
// NewLogger returns a generic log handler that can handle all type of messages. // NewLogger returns a generic log handler that can handle all type of messages.
@ -31,8 +32,8 @@ func NewLogger(logWriterCreator WriterCreator) Handler {
return &generalLogger{ return &generalLogger{
creator: logWriterCreator, creator: logWriterCreator,
buffer: make(chan Message, 16), buffer: make(chan Message, 16),
access: signal.NewSemaphore(1), access: semaphore.New(1),
done: signal.NewDone(), done: done.New(),
} }
} }

View File

@ -7,7 +7,7 @@ import (
"v2ray.com/core/common" "v2ray.com/core/common"
"v2ray.com/core/common/buf" "v2ray.com/core/common/buf"
"v2ray.com/core/common/signal" "v2ray.com/core/common/signal/done"
) )
type ConnectionOption func(*connection) type ConnectionOption func(*connection)
@ -56,7 +56,7 @@ func ConnectionOnClose(n io.Closer) ConnectionOption {
func NewConnection(opts ...ConnectionOption) net.Conn { func NewConnection(opts ...ConnectionOption) net.Conn {
c := &connection{ c := &connection{
done: signal.NewDone(), done: done.New(),
local: &net.TCPAddr{ local: &net.TCPAddr{
IP: []byte{0, 0, 0, 0}, IP: []byte{0, 0, 0, 0},
Port: 0, Port: 0,
@ -77,7 +77,7 @@ func NewConnection(opts ...ConnectionOption) net.Conn {
type connection struct { type connection struct {
reader *buf.BufferedReader reader *buf.BufferedReader
writer buf.Writer writer buf.Writer
done *signal.Done done *done.Instance
onClose io.Closer onClose io.Closer
local Addr local Addr
remote Addr remote Addr

View File

@ -3,10 +3,11 @@ package protocol
import ( import (
"io" "io"
"v2ray.com/core/common/task"
"v2ray.com/core/common" "v2ray.com/core/common"
"v2ray.com/core/common/buf" "v2ray.com/core/common/buf"
"v2ray.com/core/common/net" "v2ray.com/core/common/net"
"v2ray.com/core/common/signal"
) )
type AddressOption func(*AddressParser) type AddressOption func(*AddressParser)
@ -153,9 +154,9 @@ func (p *AddressParser) ReadAddressPort(buffer *buf.Buffer, input io.Reader) (ne
var err error var err error
if p.portFirst { if p.portFirst {
err = signal.Execute(pTask, aTask) err = task.Run(task.Sequential(pTask, aTask))()
} else { } else {
err = signal.Execute(aTask, pTask) err = task.Run(task.Sequential(aTask, pTask))()
} }
if err != nil { if err != nil {
@ -177,21 +178,21 @@ func (p *AddressParser) writeAddress(writer io.Writer, address net.Address) erro
switch address.Family() { switch address.Family() {
case net.AddressFamilyIPv4, net.AddressFamilyIPv6: case net.AddressFamilyIPv4, net.AddressFamilyIPv6:
return signal.Execute(func() error { return task.Run(task.Sequential(func() error {
return common.Error2(writer.Write([]byte{tb})) return common.Error2(writer.Write([]byte{tb}))
}, func() error { }, func() error {
return common.Error2(writer.Write(address.IP())) return common.Error2(writer.Write(address.IP()))
}) }))()
case net.AddressFamilyDomain: case net.AddressFamilyDomain:
domain := address.Domain() domain := address.Domain()
if isDomainTooLong(domain) { if isDomainTooLong(domain) {
return newError("Super long domain is not supported: ", domain) return newError("Super long domain is not supported: ", domain)
} }
return signal.Execute(func() error { return task.Run(task.Sequential(func() error {
return common.Error2(writer.Write([]byte{tb, byte(len(domain))})) return common.Error2(writer.Write([]byte{tb, byte(len(domain))}))
}, func() error { }, func() error {
return common.Error2(writer.Write([]byte(domain))) return common.Error2(writer.Write([]byte(domain)))
}) }))()
default: default:
panic("Unknown family type.") panic("Unknown family type.")
} }
@ -207,8 +208,8 @@ func (p *AddressParser) WriteAddressPort(writer io.Writer, addr net.Address, por
} }
if p.portFirst { if p.portFirst {
return signal.Execute(pTask, aTask) return task.Run(task.Sequential(pTask, aTask))()
} }
return signal.Execute(aTask, pTask) return task.Run(task.Sequential(aTask, pTask))()
} }

View File

@ -1,25 +1,25 @@
package signal package done
import ( import (
"sync" "sync"
) )
// Done is a utility for notifications of something being done. // Instance is a utility for notifications of something being done.
type Done struct { type Instance struct {
access sync.Mutex access sync.Mutex
c chan struct{} c chan struct{}
closed bool closed bool
} }
// NewDone returns a new Done. // New returns a new Done.
func NewDone() *Done { func New() *Instance {
return &Done{ return &Instance{
c: make(chan struct{}), c: make(chan struct{}),
} }
} }
// Done returns true if Close() is called. // Done returns true if Close() is called.
func (d *Done) Done() bool { func (d *Instance) Done() bool {
select { select {
case <-d.Wait(): case <-d.Wait():
return true return true
@ -29,12 +29,12 @@ func (d *Done) Done() bool {
} }
// Wait returns a channel for waiting for done. // Wait returns a channel for waiting for done.
func (d *Done) Wait() <-chan struct{} { func (d *Instance) Wait() <-chan struct{} {
return d.c return d.c
} }
// Close marks this Done 'done'. This method may be called multiple times. All calls after first call will have no effect on its status. // Close marks this Done 'done'. This method may be called multiple times. All calls after first call will have no effect on its status.
func (d *Done) Close() error { func (d *Instance) Close() error {
d.access.Lock() d.access.Lock()
defer d.access.Unlock() defer d.access.Unlock()

View File

@ -1,47 +0,0 @@
package signal
import (
"context"
)
// Execute runs a list of tasks sequentially, returns the first error encountered or nil if all tasks pass.
func Execute(tasks ...func() error) error {
for _, task := range tasks {
if err := task(); err != nil {
return err
}
}
return nil
}
// ExecuteParallel executes a list of tasks asynchronously, returns the first error encountered or nil if all tasks pass.
func ExecuteParallel(ctx context.Context, tasks ...func() error) error {
n := len(tasks)
s := NewSemaphore(n)
done := make(chan error, 1)
for _, task := range tasks {
<-s.Wait()
go func(f func() error) {
if err := f(); err != nil {
select {
case done <- err:
default:
}
}
s.Signal()
}(task)
}
for i := 0; i < n; i++ {
select {
case <-ctx.Done():
return ctx.Err()
case err := <-done:
return err
case <-s.Wait():
}
}
return nil
}

View File

@ -1,43 +0,0 @@
package signal_test
import (
"context"
"errors"
"testing"
"time"
. "v2ray.com/core/common/signal"
. "v2ray.com/ext/assert"
)
func TestExecuteParallel(t *testing.T) {
assert := With(t)
err := ExecuteParallel(context.Background(), func() error {
time.Sleep(time.Millisecond * 200)
return errors.New("test")
}, func() error {
time.Sleep(time.Millisecond * 500)
return errors.New("test2")
})
assert(err.Error(), Equals, "test")
}
func TestExecuteParallelContextCancel(t *testing.T) {
assert := With(t)
ctx, cancel := context.WithCancel(context.Background())
err := ExecuteParallel(ctx, func() error {
time.Sleep(time.Millisecond * 2000)
return errors.New("test")
}, func() error {
time.Sleep(time.Millisecond * 5000)
return errors.New("test2")
}, func() error {
cancel()
return nil
})
assert(err.Error(), HasSubstring, "canceled")
}

View File

@ -1,27 +0,0 @@
package signal
// Semaphore is an implementation of semaphore.
type Semaphore struct {
token chan struct{}
}
// NewSemaphore create a new Semaphore with n permits.
func NewSemaphore(n int) *Semaphore {
s := &Semaphore{
token: make(chan struct{}, n),
}
for i := 0; i < n; i++ {
s.token <- struct{}{}
}
return s
}
// Wait returns a channel for acquiring a permit.
func (s *Semaphore) Wait() <-chan struct{} {
return s.token
}
// Signal releases a permit into the Semaphore.
func (s *Semaphore) Signal() {
s.token <- struct{}{}
}

View File

@ -0,0 +1,27 @@
package semaphore
// Instance is an implementation of semaphore.
type Instance struct {
token chan struct{}
}
// New create a new Semaphore with n permits.
func New(n int) *Instance {
s := &Instance{
token: make(chan struct{}, n),
}
for i := 0; i < n; i++ {
s.token <- struct{}{}
}
return s
}
// Wait returns a channel for acquiring a permit.
func (s *Instance) Wait() <-chan struct{} {
return s.token
}
// Signal releases a permit into the semaphore.
func (s *Instance) Signal() {
s.token <- struct{}{}
}

View File

@ -2,7 +2,11 @@ package signal
import ( import (
"context" "context"
"sync"
"time" "time"
"v2ray.com/core/common"
"v2ray.com/core/common/task"
) )
type ActivityUpdater interface { type ActivityUpdater interface {
@ -10,9 +14,10 @@ type ActivityUpdater interface {
} }
type ActivityTimer struct { type ActivityTimer struct {
updated chan struct{} sync.RWMutex
timeout chan time.Duration updated chan struct{}
closing chan struct{} checkTask *task.Periodic
onTimeout func()
} }
func (t *ActivityTimer) Update() { func (t *ActivityTimer) Update() {
@ -22,60 +27,52 @@ func (t *ActivityTimer) Update() {
} }
} }
func (t *ActivityTimer) SetTimeout(timeout time.Duration) { func (t *ActivityTimer) check() error {
select { select {
case <-t.closing: case <-t.updated:
case t.timeout <- timeout: default:
t.finish()
}
return nil
}
func (t *ActivityTimer) finish() {
t.Lock()
defer t.Unlock()
if t.onTimeout != nil {
t.onTimeout()
}
if t.checkTask != nil {
t.checkTask.Close()
t.checkTask = nil
} }
} }
func (t *ActivityTimer) run(ctx context.Context, cancel context.CancelFunc) { func (t *ActivityTimer) SetTimeout(timeout time.Duration) {
defer func() {
cancel()
close(t.closing)
}()
timeout := <-t.timeout
if timeout == 0 { if timeout == 0 {
return t.finish()
} }
ticker := time.NewTicker(timeout) t.Lock()
defer func() {
ticker.Stop()
}()
for { if t.checkTask != nil {
select { t.checkTask.Close() // nolint: errcheck
case <-ticker.C:
case <-ctx.Done():
return
case timeout := <-t.timeout:
if timeout == 0 {
return
}
ticker.Stop()
ticker = time.NewTicker(timeout)
continue
}
select {
case <-t.updated:
// Updated keep waiting.
default:
return
}
} }
t.checkTask = &task.Periodic{
Interval: timeout,
Execute: t.check,
}
t.Unlock()
t.Update()
common.Must(t.checkTask.Start())
} }
func CancelAfterInactivity(ctx context.Context, cancel context.CancelFunc, timeout time.Duration) *ActivityTimer { func CancelAfterInactivity(ctx context.Context, cancel context.CancelFunc, timeout time.Duration) *ActivityTimer {
timer := &ActivityTimer{ timer := &ActivityTimer{
timeout: make(chan time.Duration, 1), updated: make(chan struct{}, 1),
updated: make(chan struct{}, 1), onTimeout: cancel,
closing: make(chan struct{}),
} }
timer.timeout <- timeout timer.SetTimeout(timeout)
go timer.run(ctx, cancel)
return timer return timer
} }

View File

@ -14,16 +14,26 @@ type Periodic struct {
// OnFailure will be called when Execute returns non-nil error // OnFailure will be called when Execute returns non-nil error
OnError func(error) OnError func(error)
access sync.Mutex access sync.RWMutex
timer *time.Timer timer *time.Timer
closed bool closed bool
} }
func (t *Periodic) checkedExecute() error { func (t *Periodic) setClosed(f bool) {
t.access.Lock() t.access.Lock()
defer t.access.Unlock() t.closed = f
t.access.Unlock()
}
if t.closed { func (t *Periodic) hasClosed() bool {
t.access.RLock()
defer t.access.RUnlock()
return t.closed
}
func (t *Periodic) checkedExecute() error {
if t.hasClosed() {
return nil return nil
} }
@ -31,23 +41,23 @@ func (t *Periodic) checkedExecute() error {
return err return err
} }
t.access.Lock()
t.timer = time.AfterFunc(t.Interval, func() { t.timer = time.AfterFunc(t.Interval, func() {
if err := t.checkedExecute(); err != nil && t.OnError != nil { if err := t.checkedExecute(); err != nil && t.OnError != nil {
t.OnError(err) t.OnError(err)
} }
}) })
t.access.Unlock()
return nil return nil
} }
// Start implements common.Runnable. Start must not be called multiple times without Close being called. // Start implements common.Runnable. Start must not be called multiple times without Close being called.
func (t *Periodic) Start() error { func (t *Periodic) Start() error {
t.access.Lock() t.setClosed(false)
t.closed = false
t.access.Unlock()
if err := t.checkedExecute(); err != nil { if err := t.checkedExecute(); err != nil {
t.closed = true t.setClosed(true)
return err return err
} }

View File

@ -3,7 +3,7 @@ package task
import ( import (
"context" "context"
"v2ray.com/core/common/signal" "v2ray.com/core/common/signal/semaphore"
) )
type Task func() error type Task func() error
@ -109,7 +109,7 @@ func execute(tasks ...Task) error {
// executeParallel executes a list of tasks asynchronously, returns the first error encountered or nil if all tasks pass. // executeParallel executes a list of tasks asynchronously, returns the first error encountered or nil if all tasks pass.
func executeParallel(tasks ...Task) error { func executeParallel(tasks ...Task) error {
n := len(tasks) n := len(tasks)
s := signal.NewSemaphore(n) s := semaphore.New(n)
done := make(chan error, 1) done := make(chan error, 1)
for _, task := range tasks { for _, task := range tasks {

View File

@ -308,7 +308,7 @@ func (s *Server) handlePlainHTTP(ctx context.Context, request *http.Request, wri
return nil return nil
} }
if err := signal.ExecuteParallel(ctx, requestDone, responseDone); err != nil { if err := task.Run(task.WithContext(ctx), task.Parallel(requestDone, responseDone))(); err != nil {
pipe.CloseError(link.Reader) pipe.CloseError(link.Reader)
pipe.CloseError(link.Writer) pipe.CloseError(link.Writer)
return newError("connection ends").Base(err) return newError("connection ends").Base(err)

View File

@ -122,7 +122,8 @@ func (c *Client) Process(ctx context.Context, link *core.Link, dialer proxy.Dial
return buf.Copy(responseReader, link.Writer, buf.UpdateActivity(timer)) return buf.Copy(responseReader, link.Writer, buf.UpdateActivity(timer))
} }
if err := signal.ExecuteParallel(ctx, requestDone, responseDone); err != nil { var responseDoneAndCloseWriter = task.Single(responseDone, task.OnSuccess(task.Close(link.Writer)))
if err := task.Run(task.WithContext(ctx), task.Parallel(requestDone, responseDoneAndCloseWriter))(); err != nil {
return newError("connection ends").Base(err) return newError("connection ends").Base(err)
} }

View File

@ -9,7 +9,7 @@ import (
"v2ray.com/core/common" "v2ray.com/core/common"
"v2ray.com/core/common/net" "v2ray.com/core/common/net"
"v2ray.com/core/common/serial" "v2ray.com/core/common/serial"
"v2ray.com/core/common/signal" "v2ray.com/core/common/signal/done"
"v2ray.com/core/transport/internet" "v2ray.com/core/transport/internet"
"v2ray.com/core/transport/internet/tls" "v2ray.com/core/transport/internet/tls"
) )
@ -31,7 +31,7 @@ func (l *Listener) Close() error {
type flushWriter struct { type flushWriter struct {
w io.Writer w io.Writer
d *signal.Done d *done.Instance
} }
func (fw flushWriter) Write(p []byte) (n int, err error) { func (fw flushWriter) Write(p []byte) (n int, err error) {
@ -75,7 +75,7 @@ func (l *Listener) ServeHTTP(writer http.ResponseWriter, request *http.Request)
} }
} }
done := signal.NewDone() done := done.New()
conn := net.NewConnection( conn := net.NewConnection(
net.ConnectionOutput(request.Body), net.ConnectionOutput(request.Body),
net.ConnectionInput(flushWriter{w: writer, d: done}), net.ConnectionInput(flushWriter{w: writer, d: done}),

View File

@ -10,6 +10,7 @@ import (
"v2ray.com/core/common/buf" "v2ray.com/core/common/buf"
"v2ray.com/core/common/predicate" "v2ray.com/core/common/predicate"
"v2ray.com/core/common/signal" "v2ray.com/core/common/signal"
"v2ray.com/core/common/signal/semaphore"
) )
var ( var (
@ -121,7 +122,7 @@ type Updater struct {
shouldContinue predicate.Predicate shouldContinue predicate.Predicate
shouldTerminate predicate.Predicate shouldTerminate predicate.Predicate
updateFunc func() updateFunc func()
notifier *signal.Semaphore notifier *semaphore.Instance
} }
func NewUpdater(interval uint32, shouldContinue predicate.Predicate, shouldTerminate predicate.Predicate, updateFunc func()) *Updater { func NewUpdater(interval uint32, shouldContinue predicate.Predicate, shouldTerminate predicate.Predicate, updateFunc func()) *Updater {
@ -130,7 +131,7 @@ func NewUpdater(interval uint32, shouldContinue predicate.Predicate, shouldTermi
shouldContinue: shouldContinue, shouldContinue: shouldContinue,
shouldTerminate: shouldTerminate, shouldTerminate: shouldTerminate,
updateFunc: updateFunc, updateFunc: updateFunc,
notifier: signal.NewSemaphore(1), notifier: semaphore.New(1),
} }
return u return u
} }

View File

@ -1,13 +1,13 @@
package pipe_test package pipe_test
import ( import (
"context"
"io" "io"
"testing" "testing"
"time" "time"
"v2ray.com/core/common/task"
"v2ray.com/core/common/buf" "v2ray.com/core/common/buf"
"v2ray.com/core/common/signal"
. "v2ray.com/core/transport/pipe" . "v2ray.com/core/transport/pipe"
. "v2ray.com/ext/assert" . "v2ray.com/ext/assert"
) )
@ -68,7 +68,7 @@ func TestPipeLimitZero(t *testing.T) {
bb.Write([]byte{'a', 'b'}) bb.Write([]byte{'a', 'b'})
assert(pWriter.WriteMultiBuffer(buf.NewMultiBufferValue(bb)), IsNil) assert(pWriter.WriteMultiBuffer(buf.NewMultiBufferValue(bb)), IsNil)
err := signal.ExecuteParallel(context.Background(), func() error { err := task.Run(task.Parallel(func() error {
b := buf.New() b := buf.New()
b.Write([]byte{'c', 'd'}) b.Write([]byte{'c', 'd'})
return pWriter.WriteMultiBuffer(buf.NewMultiBufferValue(b)) return pWriter.WriteMultiBuffer(buf.NewMultiBufferValue(b))
@ -87,7 +87,7 @@ func TestPipeLimitZero(t *testing.T) {
} }
assert(rb.String(), Equals, "cd") assert(rb.String(), Equals, "cd")
return nil return nil
}) }))()
assert(err, IsNil) assert(err, IsNil)
} }