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  }