github.com/benz9527/xboot@v0.0.0-20240504061247-c23f15593274/xlog/buffer_syncer.go (about)

     1  package xlog
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"sync"
     7  	"time"
     8  
     9  	"go.uber.org/zap/zapcore"
    10  
    11  	"github.com/benz9527/xboot/lib/infra"
    12  	"github.com/benz9527/xboot/lib/list"
    13  )
    14  
    15  type logRecord struct {
    16  	startOffset uint64
    17  	length      uint64
    18  }
    19  
    20  // A buffer to store log records.
    21  // Avoid GC frequently.
    22  type xLogArena struct {
    23  	buf     []byte
    24  	size    uint64
    25  	wOffset uint64
    26  	queue   list.LinkedList[logRecord]
    27  }
    28  
    29  func (arena *xLogArena) availableBytes() uint64 {
    30  	return arena.size - arena.wOffset
    31  }
    32  
    33  func (arena *xLogArena) reset() {
    34  	if arena.wOffset == 0 {
    35  		return
    36  	}
    37  	arena.wOffset = 0
    38  }
    39  
    40  func (arena *xLogArena) release() {
    41  	arena.reset()
    42  	arena.buf = nil
    43  	arena.queue = nil
    44  }
    45  
    46  // Allocate a buffer to store a log.
    47  func (arena *xLogArena) allocate(size uint64) (uint64, bool) {
    48  	if arena.availableBytes() < size {
    49  		return /* flush first */ 0, false
    50  	}
    51  	arena.wOffset += size
    52  	return /* startup */ arena.wOffset - size, true
    53  }
    54  
    55  // Cache the log to the buffer.
    56  func (arena *xLogArena) cache(log []byte) bool {
    57  	if arena.buf == nil || arena.queue == nil {
    58  		return false
    59  	}
    60  
    61  	if offset, ok := arena.allocate(uint64(len(log))); ok {
    62  		copy(arena.buf[offset:], log)
    63  		_ = arena.queue.AppendValue(logRecord{
    64  			startOffset: offset,
    65  			length:      uint64(len(log)),
    66  		})
    67  		return true
    68  	}
    69  	return false
    70  }
    71  
    72  // Flush the buffer to the writer.
    73  func (arena *xLogArena) flush(writer io.WriteCloser) error {
    74  	if arena.queue == nil {
    75  		return nil
    76  	}
    77  
    78  	// TODO Batch bytes write in one io write.
    79  	// Batch bytes write is hard to verify each log in unit tests.
    80  	if err := arena.queue.Foreach(func(idx int64, e *list.NodeElement[logRecord]) error {
    81  		data := arena.buf[e.Value.startOffset : e.Value.startOffset+e.Value.length]
    82  		if _, err := writer.Write(data); err != nil {
    83  			return err
    84  		}
    85  		arena.queue.Remove(e)
    86  		return nil
    87  	}); err != nil {
    88  		return err
    89  	}
    90  	return nil
    91  }
    92  
    93  const (
    94  	defaultBufferSize    = 512 << 10
    95  	defaultFlushInterval = 30 * time.Second
    96  )
    97  
    98  var _ zapcore.WriteSyncer = (*xLogBufferSyncer)(nil)
    99  
   100  type xLogBufferSyncer struct {
   101  	ctx           context.Context
   102  	outWriter     io.WriteCloser
   103  	flushInterval time.Duration
   104  	arena         *xLogArena
   105  	clock         zapcore.Clock
   106  	ticker        *time.Ticker
   107  	once          sync.Once
   108  	mu            sync.Mutex
   109  }
   110  
   111  func (syncer *xLogBufferSyncer) Sync() error {
   112  	syncer.mu.Lock()
   113  	defer syncer.mu.Unlock()
   114  
   115  	err := syncer.arena.flush(syncer.outWriter)
   116  	if err != nil {
   117  		return err
   118  	}
   119  	syncer.arena.reset()
   120  	return nil
   121  }
   122  
   123  func (syncer *xLogBufferSyncer) Write(log []byte) (n int, err error) {
   124  	syncer.mu.Lock()
   125  	defer syncer.mu.Unlock()
   126  
   127  	// TODO Implemented filters or hooks to pre-process logs.
   128  	if !syncer.arena.cache(log) {
   129  		if err := syncer.arena.flush(syncer.outWriter); err != nil {
   130  			return 0, err
   131  		}
   132  		syncer.arena.reset()
   133  		if !syncer.arena.cache(log) {
   134  			if /* too long to cache */ _, err := syncer.outWriter.Write(log); err != nil {
   135  				return 0, infra.NewErrorStack("[xlog-buf-syncer] unable to cache log in buffer")
   136  			}
   137  		}
   138  	}
   139  	return len(log), nil
   140  }
   141  
   142  func (syncer *xLogBufferSyncer) flushLoop() {
   143  	for {
   144  		select {
   145  		case <-syncer.ctx.Done():
   146  			_ = syncer.Sync()
   147  			syncer.ticker.Stop()
   148  			syncer.arena.release()
   149  			return
   150  		case <-syncer.ticker.C:
   151  			_ = syncer.Sync()
   152  		}
   153  	}
   154  }
   155  
   156  func (syncer *xLogBufferSyncer) initialize() {
   157  	syncer.once.Do(func() {
   158  		if syncer.arena == nil || syncer.arena.size == 0 {
   159  			syncer.arena = &xLogArena{
   160  				size:  defaultBufferSize,
   161  				buf:   make([]byte, defaultBufferSize),
   162  				queue: list.NewLinkedList[logRecord](),
   163  			}
   164  		} else if syncer.arena.size > 0 {
   165  			syncer.arena.buf = make([]byte, syncer.arena.size)
   166  			syncer.arena.queue = list.NewLinkedList[logRecord]()
   167  		}
   168  
   169  		if syncer.flushInterval == 0 {
   170  			syncer.flushInterval = defaultFlushInterval
   171  		}
   172  
   173  		if syncer.clock == nil {
   174  			syncer.clock = zapcore.DefaultClock
   175  		}
   176  
   177  		if syncer.ticker == nil {
   178  			syncer.ticker = syncer.clock.NewTicker(syncer.flushInterval)
   179  		}
   180  		go syncer.flushLoop()
   181  	})
   182  }
   183  
   184  func XLogBufferSyncer(
   185  	ctx context.Context,
   186  	writer io.WriteCloser,
   187  	bufSize uint64,
   188  	flushInterval int64,
   189  ) zapcore.WriteSyncer {
   190  	if writer == nil || ctx == nil {
   191  		return nil
   192  	}
   193  	syncer := &xLogBufferSyncer{
   194  		outWriter: writer,
   195  		arena: &xLogArena{
   196  			size: bufSize,
   197  		},
   198  		flushInterval: time.Duration(flushInterval) * time.Millisecond,
   199  		ctx:           ctx,
   200  	}
   201  	syncer.initialize()
   202  	return syncer
   203  }