zrepl/util/circlog/circlog.go
Ross Williams 729c83ee72 pre- and post-snapshot hooks
* stack-based execution model, documented in documentation
* circbuf for capturing hook output
* built-in hooks for postgres and mysql
* refactor docs, too much info on the jobs page, too difficult
  to discover snapshotting & hooks

Co-authored-by: Ross Williams <ross@ross-williams.net>
Co-authored-by: Christian Schwarz <me@cschwarz.com>

fixes #74
2019-09-27 21:25:59 +02:00

168 lines
3.5 KiB
Go

package circlog
import (
"fmt"
"math/bits"
"sync"
)
const CIRCULARLOG_INIT_SIZE int = 32 << 10
type CircularLog struct {
buf []byte
size int
max int
written int
writeCursor int
/*
Mutex prevents:
concurrent writes:
buf, size, written, writeCursor in Write([]byte)
buf, writeCursor in Reset()
data races vs concurrent Write([]byte) calls:
size in Size()
size, writeCursor in Len()
buf, size, written, writeCursor in Bytes()
*/
mtx sync.Mutex
}
func NewCircularLog(max int) (*CircularLog, error) {
if max <= 0 {
return nil, fmt.Errorf("max must be positive")
}
return &CircularLog{
size: CIRCULARLOG_INIT_SIZE,
buf: make([]byte, CIRCULARLOG_INIT_SIZE),
max: max,
}, nil
}
func nextPow2Int(n int) int {
if n < 1 {
panic("can only round up positive integers")
}
r := uint(n)
// Credit: https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
r--
// Assume at least a 32 bit integer
r |= r >> 1
r |= r >> 2
r |= r >> 4
r |= r >> 8
r |= r >> 16
if bits.UintSize == 64 {
r |= r >> 32
}
r++
// Can't exceed max positive int value
if r > ^uint(0)>>1 {
panic("rounded to larger than int()")
}
return int(r)
}
func (cl *CircularLog) Write(data []byte) (int, error) {
cl.mtx.Lock()
defer cl.mtx.Unlock()
n := len(data)
// Keep growing the buffer by doubling until
// hitting cl.max
bufAvail := cl.size - cl.writeCursor
// If cl.writeCursor wrapped on the last write,
// then the buffer is full, not empty.
if cl.writeCursor == 0 && cl.written > 0 {
bufAvail = 0
}
if n > bufAvail && cl.size < cl.max {
// Add to size, not writeCursor, so as
// to not resize multiple times if this
// Write() immediately fills up the buffer
newSize := nextPow2Int(cl.size + n)
if newSize > cl.max {
newSize = cl.max
}
newBuf := make([]byte, newSize)
// Reset write cursor to old size if wrapped
if cl.writeCursor == 0 && cl.written > 0 {
cl.writeCursor = cl.size
}
copy(newBuf, cl.buf[:cl.writeCursor])
cl.buf = newBuf
cl.size = newSize
}
// If data to be written is larger than the max size,
// discard all but the last cl.size bytes
if n > cl.size {
data = data[n-cl.size:]
// Overwrite the beginning of data
// with a string indicating truncation
copy(data, []byte("(...)"))
}
// First copy data to the end of buf. If that wasn't
// all of data, then copy the rest to the beginning
// of buf.
copied := copy(cl.buf[cl.writeCursor:], data)
if copied < n {
copied += copy(cl.buf, data[copied:])
}
cl.writeCursor = ((cl.writeCursor + copied) % cl.size)
cl.written += copied
return copied, nil
}
func (cl *CircularLog) Size() int {
cl.mtx.Lock()
defer cl.mtx.Unlock()
return cl.size
}
func (cl *CircularLog) Len() int {
cl.mtx.Lock()
defer cl.mtx.Unlock()
if cl.written >= cl.size {
return cl.size
} else {
return cl.writeCursor
}
}
func (cl *CircularLog) TotalWritten() int {
cl.mtx.Lock()
defer cl.mtx.Unlock()
return cl.written
}
func (cl *CircularLog) Reset() {
cl.mtx.Lock()
defer cl.mtx.Unlock()
cl.writeCursor = 0
cl.buf = make([]byte, CIRCULARLOG_INIT_SIZE)
cl.size = CIRCULARLOG_INIT_SIZE
}
func (cl *CircularLog) Bytes() []byte {
cl.mtx.Lock()
defer cl.mtx.Unlock()
switch {
case cl.written >= cl.size && cl.writeCursor == 0:
return cl.buf
case cl.written > cl.size:
ret := make([]byte, cl.size)
copy(ret, cl.buf[cl.writeCursor:])
copy(ret[cl.size-cl.writeCursor:], cl.buf[:cl.writeCursor])
return ret
default:
return cl.buf[:cl.writeCursor]
}
}
func (cl *CircularLog) String() string {
return string(cl.Bytes())
}