github.com/bigcommerce/nomad@v0.9.3-bc/client/lib/streamframer/framer.go (about) 1 package framer 2 3 import ( 4 "bytes" 5 "fmt" 6 "sync" 7 "time" 8 ) 9 10 var ( 11 // HeartbeatStreamFrame is the StreamFrame to send as a heartbeat, avoiding 12 // creating many instances of the empty StreamFrame 13 HeartbeatStreamFrame = &StreamFrame{} 14 ) 15 16 // StreamFrame is used to frame data of a file when streaming 17 type StreamFrame struct { 18 // Offset is the offset the data was read from 19 Offset int64 `json:",omitempty"` 20 21 // Data is the read data 22 Data []byte `json:",omitempty"` 23 24 // File is the file that the data was read from 25 File string `json:",omitempty"` 26 27 // FileEvent is the last file event that occurred that could cause the 28 // streams position to change or end 29 FileEvent string `json:",omitempty"` 30 } 31 32 // IsHeartbeat returns if the frame is a heartbeat frame 33 func (s *StreamFrame) IsHeartbeat() bool { 34 return s.Offset == 0 && len(s.Data) == 0 && s.File == "" && s.FileEvent == "" 35 } 36 37 func (s *StreamFrame) Clear() { 38 s.Offset = 0 39 s.Data = nil 40 s.File = "" 41 s.FileEvent = "" 42 } 43 44 func (s *StreamFrame) IsCleared() bool { 45 if s.Offset != 0 { 46 return false 47 } else if s.Data != nil { 48 return false 49 } else if s.File != "" { 50 return false 51 } else if s.FileEvent != "" { 52 return false 53 } else { 54 return true 55 } 56 } 57 58 func (s *StreamFrame) Copy() *StreamFrame { 59 n := new(StreamFrame) 60 *n = *s 61 n.Data = make([]byte, len(s.Data)) 62 copy(n.Data, s.Data) 63 return n 64 } 65 66 // StreamFramer is used to buffer and send frames as well as heartbeat. 67 type StreamFramer struct { 68 // out is where frames are sent and is closed when no more frames will 69 // be sent. 70 out chan<- *StreamFrame 71 72 frameSize int 73 74 heartbeat *time.Ticker 75 flusher *time.Ticker 76 77 // shutdown is true when a shutdown is triggered 78 shutdown bool 79 80 // shutdownCh is closed when no more Send()s will be called and run() 81 // should flush pending frames before closing exitCh 82 shutdownCh chan struct{} 83 84 // exitCh is closed when the run() goroutine exits and no more frames 85 // will be sent. 86 exitCh chan struct{} 87 88 // The mutex protects everything below 89 l sync.Mutex 90 91 // The current working frame 92 f *StreamFrame 93 data *bytes.Buffer 94 95 // Captures whether the framer is running 96 running bool 97 } 98 99 // NewStreamFramer creates a new stream framer that will output StreamFrames to 100 // the passed output channel. 101 func NewStreamFramer(out chan<- *StreamFrame, 102 heartbeatRate, batchWindow time.Duration, frameSize int) *StreamFramer { 103 104 // Create the heartbeat and flush ticker 105 heartbeat := time.NewTicker(heartbeatRate) 106 flusher := time.NewTicker(batchWindow) 107 108 return &StreamFramer{ 109 out: out, 110 frameSize: frameSize, 111 heartbeat: heartbeat, 112 flusher: flusher, 113 f: new(StreamFrame), 114 data: bytes.NewBuffer(make([]byte, 0, 2*frameSize)), 115 shutdownCh: make(chan struct{}), 116 exitCh: make(chan struct{}), 117 } 118 } 119 120 // Destroy is used to cleanup the StreamFramer and flush any pending frames 121 func (s *StreamFramer) Destroy() { 122 s.l.Lock() 123 124 wasShutdown := s.shutdown 125 s.shutdown = true 126 127 if !wasShutdown { 128 close(s.shutdownCh) 129 } 130 131 s.heartbeat.Stop() 132 s.flusher.Stop() 133 running := s.running 134 s.l.Unlock() 135 136 // Ensure things were flushed 137 if running { 138 <-s.exitCh 139 } 140 141 // Close out chan only after exitCh has exited 142 if !wasShutdown { 143 close(s.out) 144 } 145 } 146 147 // Run starts a long lived goroutine that handles sending data as well as 148 // heartbeating 149 func (s *StreamFramer) Run() { 150 s.l.Lock() 151 defer s.l.Unlock() 152 if s.running { 153 return 154 } 155 156 s.running = true 157 go s.run() 158 } 159 160 // ExitCh returns a channel that will be closed when the run loop terminates. 161 func (s *StreamFramer) ExitCh() <-chan struct{} { 162 return s.exitCh 163 } 164 165 // run is the internal run method. It exits if Destroy is called or an error 166 // occurs, in which case the exit channel is closed. 167 func (s *StreamFramer) run() { 168 defer func() { 169 s.l.Lock() 170 s.running = false 171 s.l.Unlock() 172 close(s.exitCh) 173 }() 174 175 OUTER: 176 for { 177 select { 178 case <-s.shutdownCh: 179 break OUTER 180 case <-s.flusher.C: 181 // Skip if there is nothing to flush 182 s.l.Lock() 183 if s.f.IsCleared() { 184 s.l.Unlock() 185 continue 186 } 187 188 // Read the data for the frame, and send it 189 s.send() 190 s.l.Unlock() 191 case <-s.heartbeat.C: 192 // Send a heartbeat frame 193 select { 194 case s.out <- HeartbeatStreamFrame: 195 case <-s.shutdownCh: 196 } 197 } 198 } 199 200 s.l.Lock() 201 // Send() may have left a partial frame. Send it now. 202 if !s.f.IsCleared() { 203 s.f.Data = s.readData() 204 205 // Only send if there's actually data left 206 if len(s.f.Data) > 0 { 207 // Cannot select on shutdownCh as it's already closed 208 // Cannot select on exitCh as it's only closed after this exits 209 s.out <- s.f.Copy() 210 } 211 } 212 s.l.Unlock() 213 } 214 215 // send takes a StreamFrame, encodes and sends it 216 func (s *StreamFramer) send() { 217 // Ensure s.out has not already been closd by Destroy 218 select { 219 case <-s.exitCh: 220 return 221 default: 222 } 223 224 s.f.Data = s.readData() 225 select { 226 case s.out <- s.f.Copy(): 227 s.f.Clear() 228 case <-s.exitCh: 229 } 230 } 231 232 // readData is a helper which reads the buffered data returning up to the frame 233 // size of data. Must be called with the lock held. The returned value is 234 // invalid on the next read or write into the StreamFramer buffer 235 func (s *StreamFramer) readData() []byte { 236 // Compute the amount to read from the buffer 237 size := s.data.Len() 238 if size > s.frameSize { 239 size = s.frameSize 240 } 241 if size == 0 { 242 return nil 243 } 244 d := s.data.Next(size) 245 return d 246 } 247 248 // Send creates and sends a StreamFrame based on the passed parameters. An error 249 // is returned if the run routine hasn't run or encountered an error. Send is 250 // asynchronous and does not block for the data to be transferred. 251 func (s *StreamFramer) Send(file, fileEvent string, data []byte, offset int64) error { 252 s.l.Lock() 253 defer s.l.Unlock() 254 // If we are not running, return the error that caused us to not run or 255 // indicated that it was never started. 256 if !s.running { 257 return fmt.Errorf("StreamFramer not running") 258 } 259 260 // Check if not mergeable 261 if !s.f.IsCleared() && (s.f.File != file || s.f.FileEvent != fileEvent) { 262 // Flush the old frame 263 s.send() 264 } 265 266 // Store the new data as the current frame. 267 if s.f.IsCleared() { 268 s.f.Offset = offset 269 s.f.File = file 270 s.f.FileEvent = fileEvent 271 } 272 273 // Write the data to the buffer 274 s.data.Write(data) 275 276 // Handle the delete case in which there is no data 277 force := s.data.Len() == 0 && s.f.FileEvent != "" 278 279 // Flush till we are under the max frame size 280 for s.data.Len() >= s.frameSize || force { 281 // Clear since are flushing the frame and capturing the file event. 282 // Subsequent data frames will be flushed based on the data size alone 283 // since they share the same fileevent. 284 if force { 285 force = false 286 } 287 288 // Ensure s.out has not already been closed by Destroy 289 select { 290 case <-s.exitCh: 291 return nil 292 default: 293 } 294 295 // Create a new frame to send it 296 s.f.Data = s.readData() 297 select { 298 case s.out <- s.f.Copy(): 299 case <-s.exitCh: 300 return nil 301 } 302 303 // Update the offset 304 s.f.Offset += int64(len(s.f.Data)) 305 } 306 307 return nil 308 }