go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/client/butler/bundler/stream_test.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package bundler 16 17 import ( 18 "bytes" 19 "errors" 20 "fmt" 21 "strconv" 22 "testing" 23 "time" 24 25 . "github.com/smartystreets/goconvey/convey" 26 "go.chromium.org/luci/common/clock/testclock" 27 "go.chromium.org/luci/logdog/api/logpb" 28 ) 29 30 type testParserCommand struct { 31 // data is the data content of this command. 32 data []byte 33 // ts is the timestamp, which is valid if this is a data command. 34 ts time.Time 35 // splitToggle, if not null, causes this command to set the "allowSplit" 36 // parser constraint. 37 splitToggle *bool 38 // closedToggle, if not null, causes this command to set the "closed" 39 // parser constraint. 40 closedToggle *bool 41 // err, if not nil, is returned when this command is encountered. 42 err error 43 } 44 45 var errTestInduced = errors.New("test error") 46 47 // testParser is a parser implementation that allows specifically-configured 48 // data to be emitted. It consumes commands, some of which alter its behavior 49 // and others of which present data. The resulting state affects how it emits 50 // LogEntry records via nextEntry. 51 type testParser struct { 52 commands []*testParserCommand 53 54 truncateOn bool 55 closedOn bool 56 err error 57 58 // nextIndex is the next stream index to assign. 59 nextIndex uint64 60 } 61 62 func (p *testParser) addCommand(r *testParserCommand) { 63 p.commands = append(p.commands, r) 64 } 65 66 func (p *testParser) nextCommand(pop bool) *testParserCommand { 67 if len(p.commands) == 0 { 68 return nil 69 } 70 cmd := p.commands[0] 71 if pop { 72 p.commands = p.commands[1:] 73 } 74 return cmd 75 } 76 77 func (p *testParser) popData() (r *testParserCommand) { 78 for i, cmd := range p.commands { 79 if cmd.data != nil { 80 p.commands = p.commands[i+1:] 81 return cmd 82 } 83 } 84 return nil 85 } 86 87 func (p *testParser) tags(ts time.Time, commands ...string) { 88 for _, c := range commands { 89 p.addTag(c, ts) 90 } 91 } 92 93 func (p *testParser) addError(err error) { 94 p.addCommand(&testParserCommand{ 95 err: err, 96 }) 97 } 98 99 func (p *testParser) addTag(tag string, ts time.Time) { 100 p.addData([]byte(tag), ts) 101 } 102 103 func (p *testParser) addData(d []byte, ts time.Time) { 104 p.addCommand(&testParserCommand{ 105 data: d, 106 ts: ts, 107 }) 108 } 109 110 func (p *testParser) setAllowSplit(value bool) { 111 p.addCommand(&testParserCommand{ 112 splitToggle: &value, 113 }) 114 } 115 116 func (p *testParser) appendData(d Data) { 117 p.addData(d.Bytes(), d.Timestamp()) 118 } 119 120 func (p *testParser) nextEntry(c *constraints) (*logpb.LogEntry, error) { 121 // Process records until we hit data or run out. 122 for p.err == nil { 123 rec := p.nextCommand(false) 124 if rec == nil { 125 return nil, p.err 126 } 127 128 // If this is a data record, process. 129 if rec.data != nil { 130 break 131 } 132 133 // Ingest commands, repeat. 134 if rec.err != nil { 135 p.err = rec.err 136 break 137 } 138 139 if rec.splitToggle != nil { 140 p.truncateOn = *rec.splitToggle 141 } 142 if rec.closedToggle != nil { 143 p.closedOn = *rec.closedToggle 144 } 145 p.nextCommand(true) 146 } 147 148 if p.err != nil { 149 return nil, p.err 150 } 151 152 // This is a data record. If we're configured to not yield it, leave it and 153 // return nil. 154 if p.truncateOn && (!c.allowSplit || (p.closedOn && !c.closed)) { 155 return nil, nil 156 } 157 158 // Consume this record. 159 rec := p.nextCommand(true) 160 le := logpb.LogEntry{ 161 StreamIndex: p.nextIndex, 162 Content: &logpb.LogEntry_Text{Text: &logpb.Text{ 163 Lines: []*logpb.Text_Line{ 164 {Value: append([]byte(nil), rec.data...)}, 165 }, 166 }}, 167 } 168 p.nextIndex++ 169 return &le, nil 170 } 171 172 func (p *testParser) bufferedBytes() (r int64) { 173 for _, rec := range p.commands { 174 r += int64(len(rec.data)) 175 } 176 return 177 } 178 179 func (p *testParser) firstChunkTime() (time.Time, bool) { 180 for _, c := range p.commands { 181 if c.data != nil { 182 return c.ts, true 183 } 184 } 185 return time.Time{}, false 186 } 187 188 func TestStream(t *testing.T) { 189 Convey(`A testing stream config`, t, func() { 190 tc := testclock.New(time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC)) 191 tp := testParser{} 192 c := streamConfig{ 193 name: "test", 194 parser: &tp, 195 template: logpb.ButlerLogBundle_Entry{ 196 Desc: &logpb.LogStreamDescriptor{ 197 Prefix: "test-prefix", 198 Name: "test", 199 }, 200 }, 201 } 202 203 Convey(`With a 64-byte maximum buffer and 1 second maximum duration`, func() { 204 c.maximumBufferedBytes = 64 205 c.maximumBufferDuration = time.Second 206 s := newStream(c) 207 208 Convey(`Is not drained by default`, func() { 209 So(s.isDrained(), ShouldBeFalse) 210 }) 211 212 Convey(`With no data, has no expiration time.`, func() { 213 _, has := s.expireTime() 214 So(has, ShouldBeFalse) 215 }) 216 217 Convey(`Append will ignore a 0-byte chunk.`, func() { 218 d := data(tc.Now()) 219 So(s.Append(d), ShouldBeNil) 220 So(d.released, ShouldBeTrue) 221 }) 222 223 Convey(`Append will add two 32-byte chunks.`, func() { 224 content := bytes.Repeat([]byte{0xAA}, 32) 225 So(s.Append(data(tc.Now(), content...)), ShouldBeNil) 226 So(s.Append(data(tc.Now(), content...)), ShouldBeNil) 227 }) 228 229 Convey(`Append will add a large chunk when there are no other Data blocks.`, func() { 230 d := data(tc.Now(), bytes.Repeat([]byte{0xAA}, 128)...) 231 So(s.Append(d), ShouldBeNil) 232 233 Convey(`Will use that data's timestamp as expiration time.`, func() { 234 t, has := s.expireTime() 235 So(has, ShouldBeTrue) 236 So(t.Equal(tc.Now().Add(time.Second)), ShouldBeTrue) 237 }) 238 }) 239 240 Convey(`Append will block if the chunk exceeds the buffer size.`, func() { 241 signalC := make(chan struct{}) 242 s.c.onAppend = func(appended bool) { 243 if !appended { 244 // We're waiting. 245 close(signalC) 246 } 247 } 248 249 // Add one chunk so we don't hit the "only byte" condition. 250 So(s.Append(data(tc.Now(), bytes.Repeat([]byte{0xAA}, 34)...)), ShouldBeNil) 251 252 // Wait until we get the signal that Append() will block, then consume 253 // some data and unblock Append(). 254 blocked := false 255 go func() { 256 <-signalC 257 258 s.withParserLock(func() error { 259 tp.popData() 260 return nil 261 }) 262 blocked = true 263 s.signalDataConsumed() 264 }() 265 266 // Add one chunk so we don't hit the "only byte" condition. 267 So(s.Append(data(tc.Now(), bytes.Repeat([]byte{0xBB}, 32)...)), ShouldBeNil) 268 So(blocked, ShouldBeTrue) 269 }) 270 271 Convey(`Append in an error state`, func() { 272 terr := errors.New("test error") 273 274 Convey(`Will return the error state.`, func() { 275 s.appendErr = terr 276 277 d := data(tc.Now(), bytes.Repeat([]byte{0xAA}, 32)...) 278 So(s.Append(d), ShouldEqual, terr) 279 So(d.released, ShouldBeTrue) 280 }) 281 282 Convey(`Will block if the chunk exceeds buffer size, and return error state.`, func() { 283 signalC := make(chan struct{}) 284 s.c.onAppend = func(appended bool) { 285 if !appended { 286 // Waiting, notify our goroutine that we're going to be waiting. 287 close(signalC) 288 } 289 } 290 291 // Add one chunk so we don't hit the "only byte" condition. 292 So(s.Append(data(tc.Now(), bytes.Repeat([]byte{0xAA}, 34)...)), ShouldBeNil) 293 294 // Wait until we get the signal that Append() will block, then consume 295 // some data and unblock Append(). 296 go func() { 297 <-signalC 298 299 s.stateLock.Lock() 300 defer s.stateLock.Unlock() 301 s.setAppendErrorLocked(terr) 302 }() 303 304 // Add one chunk so we don't hit the "only byte" condition. 305 for _, sz := range []int{32, 1, 0} { 306 d := data(tc.Now(), bytes.Repeat([]byte{0xAA}, sz)...) 307 So(s.Append(d), ShouldEqual, terr) 308 So(d.released, ShouldBeTrue) 309 } 310 }) 311 }) 312 }) 313 314 Convey(`When building bundle entries`, func() { 315 bb := &builder{ 316 size: 1024, 317 } 318 s := newStream(c) 319 320 Convey(`Returns nil with no buffered data.`, func() { 321 So(s.nextBundleEntry(bb, false), ShouldBeFalse) 322 So(bb.bundle(), shouldHaveBundleEntries) 323 }) 324 325 Convey(`With a single record, returns that entry.`, func() { 326 tp.tags(tc.Now(), "a", "b") 327 328 So(s.nextBundleEntry(bb, false), ShouldBeTrue) 329 So(bb.bundle(), shouldHaveBundleEntries, "test:a:b") 330 }) 331 332 Convey(`When split is allowed, returns nil.`, func() { 333 tp.tags(tc.Now(), "a", "b") 334 tp.setAllowSplit(true) 335 tp.tags(tc.Now(), "c") 336 337 So(s.nextBundleEntry(bb, false), ShouldBeTrue) 338 So(bb.bundle(), shouldHaveBundleEntries, "test:a:b") 339 So(s.nextBundleEntry(bb, false), ShouldBeFalse) 340 341 So(s.nextBundleEntry(bb, true), ShouldBeTrue) 342 So(bb.bundle(), shouldHaveBundleEntries, "test:a:b:c") 343 }) 344 345 Convey(`When an error occurs during stream parsing, drains stream.`, func() { 346 So(s.isDrained(), ShouldBeFalse) 347 tp.tags(tc.Now(), "a") 348 tp.addError(errTestInduced) 349 tp.tags(tc.Now(), "b") 350 351 So(s.nextBundleEntry(bb, false), ShouldBeTrue) 352 So(s.isDrained(), ShouldBeTrue) 353 So(bb.bundle(), shouldHaveBundleEntries, "+test:a") 354 So(s.nextBundleEntry(bb, false), ShouldBeFalse) 355 }) 356 357 Convey(`With only an error, returns no bundle entry.`, func() { 358 So(s.isDrained(), ShouldBeFalse) 359 tp.addError(errTestInduced) 360 tp.tags(tc.Now(), "a") 361 tp.tags(tc.Now(), "b") 362 363 So(s.nextBundleEntry(bb, false), ShouldBeFalse) 364 So(bb.bundle(), shouldHaveBundleEntries) 365 So(s.isDrained(), ShouldBeTrue) 366 }) 367 }) 368 }) 369 } 370 371 // TestStreamSmoke tests a Stream in an actual multi-goroutine workflow. 372 func TestStreamSmoke(t *testing.T) { 373 Convey(`When running a smoke test`, t, func() { 374 tc := testclock.New(time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC)) 375 tp := testParser{} 376 c := streamConfig{ 377 name: "test", 378 parser: &tp, 379 template: logpb.ButlerLogBundle_Entry{ 380 Desc: &logpb.LogStreamDescriptor{ 381 Prefix: "test-prefix", 382 Name: "test", 383 }, 384 }, 385 } 386 s := newStream(c) 387 388 // Appender goroutine, constantly appends data. 389 // 390 // This will be inherently throttled by the nextBundle consumption. 391 dataTokenC := make(chan struct{}, 512) 392 go func() { 393 defer func() { 394 close(dataTokenC) 395 s.Close() 396 }() 397 398 for i := 0; i < 512; i++ { 399 s.Append(data(tc.Now(), []byte(fmt.Sprintf("%d", i))...)) 400 401 // Note that data has been sent. 402 dataTokenC <- struct{}{} 403 } 404 }() 405 406 // The consumer goroutine will consume bundles from the stream. 407 consumerC := make(chan struct{}) 408 bundleC := make(chan *logpb.ButlerLogBundle) 409 for i := 0; i < 32; i++ { 410 go func() { 411 defer func() { 412 consumerC <- struct{}{} 413 }() 414 415 b := (*builder)(nil) 416 for !s.isDrained() { 417 if b == nil { 418 b = &builder{ 419 size: 128, 420 } 421 } 422 423 s.nextBundleEntry(b, false) 424 if b.hasContent() { 425 bundleC <- b.bundle() 426 b = nil 427 } else { 428 // No content! Sleep for a second and check again. 429 <-dataTokenC 430 } 431 } 432 }() 433 } 434 435 // Collect all bundles. 436 gotIt := map[int]struct{}{} 437 collectDoneC := make(chan struct{}) 438 go func() { 439 defer close(collectDoneC) 440 441 for bundle := range bundleC { 442 for _, be := range bundle.Entries { 443 for _, le := range be.Logs { 444 idx, _ := strconv.Atoi(logEntryName(le)) 445 gotIt[idx] = struct{}{} 446 } 447 } 448 } 449 }() 450 451 // Awaken all sleeping goroutines. 452 tc.Add(32 * time.Second) 453 for i := 0; i < 32; i++ { 454 <-consumerC 455 } 456 close(bundleC) 457 458 // Did we get them all? 459 <-collectDoneC 460 for i := 0; i < 512; i++ { 461 _, ok := gotIt[i] 462 So(ok, ShouldBeTrue) 463 } 464 }) 465 }