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 }