Files
mochi-mqtt/circ/buffer.go
2019-10-27 11:00:50 +00:00

208 lines
5.1 KiB
Go

package circ
import (
"io"
"sync"
"sync/atomic"
"github.com/mochi-co/mqtt/debug"
)
var (
// bufferSize is the default size of the buffer.
bufferSize int64 = 1024 * 256
// blockSize is the default size of bytes per R/W block.
blockSize int64 = 2048
)
// buffer contains core values and methods to be included in a reader or writer.
type buffer struct {
sync.RWMutex
// id is the identifier of the buffer. This is used in debug output.
id string
// size is the size of the buffer.
size int64
// block is the size of the R/W block.
block int64
// buffer is the circular byte buffer.
buf []byte
// tmp is a temporary buffer.
tmp []byte
// head is the current position in the sequence.
head int64
// tail is the committed position in the sequence, I.E. where we
// have successfully consumed and processed to.
tail int64
// rcond is the sync condition for the reader.
rcond *sync.Cond
// done indicates that the buffer is closed.
done int64
// wcond is the sync condition for the writer.
wcond *sync.Cond
}
// newBuffer returns a new instance of buffer.
func newBuffer(size, block int64) buffer {
if size == 0 {
size = bufferSize
}
if block == 0 {
block = blockSize
}
if size < 2*block {
size = 2 * block
}
return buffer{
size: size,
block: block,
buf: make([]byte, size),
tmp: make([]byte, size),
rcond: sync.NewCond(new(sync.Mutex)),
wcond: sync.NewCond(new(sync.Mutex)),
}
}
// Close notifies the buffer event loops to stop.
func (b *buffer) Close() {
atomic.StoreInt64(&b.done, 1)
debug.Println(b.id, "[B] STORE done=1 buffer closing")
b.wcond.L.Lock()
b.wcond.Broadcast()
b.wcond.L.Unlock()
b.rcond.L.Lock()
b.rcond.Broadcast()
b.rcond.L.Unlock()
debug.Println(b.id, "[B] DONE REBROADCASTED")
}
// Get will return the tail and head positions of the buffer.
func (b *buffer) GetPos() (int64, int64) {
return atomic.LoadInt64(&b.tail), atomic.LoadInt64(&b.head)
}
// SetPos sets the head and tail of the buffer. This method should only be
// used for testing.
func (b *buffer) SetPos(tail, head int64) {
b.tail, b.head = tail, head
}
// Set writes bytes to a byte buffer. This method should only be used for testing
// and will panic if out of range.
func (b *buffer) Set(p []byte, start, end int) {
o := 0
for i := start; i < end; i++ {
b.buf[i] = p[o]
o++
}
}
// awaitCapacity will hold until there are n bytes free in the buffer, blocking
// until there are at least n bytes to write without overrunning the tail.
func (b *buffer) awaitCapacity(n int64) (head int64, err error) {
head = atomic.LoadInt64(&b.head)
next := head + n
wrapped := next - b.size
tail := atomic.LoadInt64(&b.tail)
debug.Println(b.id, "[B] awaiting capacity (n)", n)
b.rcond.L.Lock()
for ; wrapped > tail || (tail > head && next > tail && wrapped < 0); tail = atomic.LoadInt64(&b.tail) {
debug.Println(b.id, "[B] iter no capacity")
//fmt.Println("\t", wrapped, ">", tail, wrapped > tail, "||", tail, ">", head, "&&", next, ">", tail, "&&", wrapped, "<", 0, (tail > head && next > tail && wrapped < 0))
if atomic.LoadInt64(&b.done) == 1 {
return 0, io.EOF
}
debug.Println(b.id, "[B] iter no capacity waiting")
b.rcond.Wait()
}
b.rcond.L.Unlock()
debug.Println(b.id, "[B] capacity unlocked (tail)", tail)
return
}
// awaitFilled will hold until there are at least n bytes waiting between the
// tail and head.
func (b *buffer) awaitFilled(n int64) (tail int64, err error) {
head := atomic.LoadInt64(&b.head)
tail = atomic.LoadInt64(&b.tail)
debug.Println(b.id, "[B] awaiting filled (tail, head)", tail, head)
b.wcond.L.Lock()
for ; head > tail && tail+n > head || head < tail && b.size-tail+head < n; head = atomic.LoadInt64(&b.head) {
debug.Println(b.id, "[B] iter no fill")
if atomic.LoadInt64(&b.done) == 1 {
return 0, io.EOF
}
debug.Println(b.id, "[B] iter no fill waiting")
b.wcond.Wait()
}
b.wcond.L.Unlock()
debug.Println(b.id, "[B] filled (head)", head)
return
}
// CommitHead moves the head position of the buffer n bytes. If there is not enough
// capacity, the method will wait until there is.
//func (b *buffer) CommitHead(n int64) error {
// return nil
//}
// CommitTail moves the tail position of the buffer n bytes, and will wait until
// there is enough capacity for at least n bytes.
func (b *buffer) CommitTail(n int64) error {
debug.Println(b.id, "[B] commit/discard (n)", n)
_, err := b.awaitFilled(n)
if err != nil {
return err
}
debug.Println(b.id, "[B] commit/discard unlocked")
tail := atomic.LoadInt64(&b.tail)
if tail+n < b.size {
atomic.StoreInt64(&b.tail, tail+n)
debug.Println(b.id, "[B] STORE TAIL", tail+n)
} else {
atomic.StoreInt64(&b.tail, (tail+n)%b.size)
debug.Println(b.id, "[B] STORE TAIL (wrapped)", (tail+n)%b.size)
}
b.rcond.L.Lock()
b.rcond.Broadcast()
b.rcond.L.Unlock()
return nil
}
// capDelta returns the difference between the head and tail.
func (b *buffer) capDelta(tail, head int64) int64 {
if head < tail {
debug.Println(b.id, "[B] capdelta (tail, head, v)", tail, head, b.size-tail+head)
return b.size - tail + head
}
debug.Println(b.id, "[B] capdelta (tail, head, v)", tail, head, head-tail)
return head - tail
}