-
Notifications
You must be signed in to change notification settings - Fork 449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update ChanReader / ChanWriter for use inside toxics #134
base: dev
Are you sure you want to change the base?
Changes from 9 commits
9ac0ec0
4e56917
a0d7647
b3650a4
85b1728
cf6492a
0482278
a323870
4c86ebd
bf1f8f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
package stream | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"io" | ||
"time" | ||
|
@@ -32,12 +33,20 @@ func NewChanWriter(output chan<- *StreamChunk) *ChanWriter { | |
// Write `buf` as a StreamChunk to the channel. The full buffer is always written, and error | ||
// will always be nil. Calling `Write()` after closing the channel will panic. | ||
func (c *ChanWriter) Write(buf []byte) (int, error) { | ||
if len(buf) == 0 { | ||
return 0, nil | ||
} | ||
|
||
packet := &StreamChunk{make([]byte, len(buf)), time.Now()} | ||
copy(packet.Data, buf) // Make a copy before sending it to the channel | ||
c.output <- packet | ||
return len(buf), nil | ||
} | ||
|
||
func (c *ChanWriter) SetOutput(output chan<- *StreamChunk) { | ||
c.output = output | ||
} | ||
|
||
// Close the output channel | ||
func (c *ChanWriter) Close() error { | ||
close(c.output) | ||
|
@@ -71,7 +80,7 @@ func (c *ChanReader) Read(out []byte) (int, error) { | |
} | ||
n := copy(out, c.buffer) | ||
c.buffer = c.buffer[n:] | ||
if len(out) <= len(c.buffer) { | ||
if len(out) == n { | ||
return n, nil | ||
} else if n > 0 { | ||
// We have some data to return, so make the channel read optional | ||
|
@@ -106,3 +115,104 @@ func (c *ChanReader) Read(out []byte) (int, error) { | |
c.buffer = p.Data[n2:] | ||
return n + n2, nil | ||
} | ||
|
||
// TransactionalReader is a ChanReader that can rollback its progress to checkpoints. | ||
// This is useful when using other buffered readers, since they may read past the end of a message. | ||
// The buffered reader can later be removed by rolling back any buffered bytes. | ||
// | ||
// chan []byte -> ChanReader -> TeeReader -> Read() -> output | ||
// V ^ | ||
// bytes.Buffer -> bytes.Reader | ||
type TransactionalReader struct { | ||
buffer *bytes.Buffer | ||
bufReader *bytes.Reader | ||
reader *ChanReader | ||
tee io.Reader | ||
} | ||
|
||
func NewTransactionalReader(input <-chan *StreamChunk) *TransactionalReader { | ||
t := &TransactionalReader{ | ||
buffer: bytes.NewBuffer(make([]byte, 0, 32*1024)), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you choose 32K sized buffer for a specific reason? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I chose 32K because it's the size used by |
||
reader: NewChanReader(input), | ||
} | ||
t.tee = io.TeeReader(t.reader, t.buffer) | ||
return t | ||
} | ||
|
||
// Reads from the input channel either directly, or from a buffer if Rollback() has been called. | ||
// If the reader returns `ErrInterrupted`, it will automatically call Rollback() | ||
func (t *TransactionalReader) Read(out []byte) (n int, err error) { | ||
defer func() { | ||
if err == ErrInterrupted { | ||
t.Rollback() | ||
} | ||
}() | ||
|
||
if t.bufReader != nil { | ||
n, err := t.bufReader.Read(out) | ||
if err == io.EOF { | ||
t.bufReader = nil | ||
if n > 0 { | ||
return n, nil | ||
} else { | ||
return t.tee.Read(out) | ||
} | ||
} | ||
return n, err | ||
} else { | ||
return t.tee.Read(out) | ||
} | ||
} | ||
|
||
// Flushes all buffers past the current position in the reader to the specified writer. | ||
func (t *TransactionalReader) FlushTo(writer io.Writer) { | ||
n := 0 | ||
if t.bufReader != nil { | ||
n = t.bufReader.Len() | ||
} | ||
buf := make([]byte, n+len(t.reader.buffer)) | ||
if n > 0 { | ||
t.bufReader.Read(buf[:n]) | ||
} | ||
if len(buf[n:]) > 0 { | ||
t.reader.Read(buf[n:]) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this correct? Why are we trying to flush the reader? It's not buffered is it? I'll look at this again later and probably understand right away. |
||
writer.Write(buf) | ||
t.bufReader = nil | ||
t.buffer.Reset() | ||
} | ||
|
||
// Sets a checkpoint in the reader. A call to Rollback() will begin reading from this point. | ||
// If offset is negative, the checkpoint will be set N bytes before the current position. | ||
// If the offset is positive, the checkpoint will be set N bytes after the previous checkpoint. | ||
// An offset of 0 will set the checkpoint to the current position. | ||
func (t *TransactionalReader) Checkpoint(offset int) { | ||
current := t.buffer.Len() | ||
if t.bufReader != nil { | ||
current = int(t.bufReader.Size()) - t.bufReader.Len() | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So
Okay I found this confusing but LGTM |
||
|
||
n := current | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Initializing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right, looks like this is redundant. I'll see if I can refactor this function and make it a little simpler. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually found another bug while looking at this. if |
||
if offset > 0 { | ||
n = offset | ||
} else { | ||
n = current + offset | ||
} | ||
|
||
if n >= current { | ||
t.buffer.Reset() | ||
} else { | ||
t.buffer.Next(n) | ||
} | ||
} | ||
|
||
// Rolls back the reader to start from the last checkpoint. | ||
func (t *TransactionalReader) Rollback() { | ||
if t.buffer.Len() > 0 { | ||
t.bufReader = bytes.NewReader(t.buffer.Bytes()) | ||
} | ||
} | ||
|
||
func (t *TransactionalReader) SetInterrupt(interrupt <-chan struct{}) { | ||
t.reader.SetInterrupt(interrupt) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why
Checkpoint(0)
? Should it not beCheckpoint(-stub.Reader.Buffered())
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example where I used
.Buffered()
was for abufio.Reader
attached to theTransactionalReader
.In this case, the amount of buffered data is always 0. Since everything read is being used, we can set the checkpoint to the last read byte, rather than the
(last read) - (amount buffered by bufio.Reader)