go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/server/collector/collector_test.go (about)

     1  // Copyright 2016 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 collector
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"fmt"
    21  	"sync/atomic"
    22  	"testing"
    23  
    24  	"go.chromium.org/luci/common/clock/testclock"
    25  	"go.chromium.org/luci/common/errors"
    26  	"go.chromium.org/luci/common/retry/transient"
    27  	"go.chromium.org/luci/config"
    28  	"go.chromium.org/luci/logdog/api/logpb"
    29  	"go.chromium.org/luci/logdog/client/pubsubprotocol"
    30  	"go.chromium.org/luci/logdog/common/storage/memory"
    31  	"go.chromium.org/luci/logdog/common/types"
    32  	cc "go.chromium.org/luci/logdog/server/collector/coordinator"
    33  
    34  	. "github.com/smartystreets/goconvey/convey"
    35  	. "go.chromium.org/luci/common/testing/assertions"
    36  )
    37  
    38  // TestCollector runs through a series of end-to-end Collector workflows and
    39  // ensures that the Collector behaves appropriately.
    40  func testCollectorImpl(t *testing.T, caching bool) {
    41  	Convey(fmt.Sprintf(`Using a test configuration with caching == %v`, caching), t, func() {
    42  		c, _ := testclock.UseTime(context.Background(), testclock.TestTimeLocal)
    43  
    44  		tcc := &testCoordinator{}
    45  		st := &testStorage{Storage: &memory.Storage{}}
    46  
    47  		coll := &Collector{
    48  			Coordinator: tcc,
    49  			Storage:     st,
    50  		}
    51  		defer coll.Close()
    52  
    53  		bb := bundleBuilder{
    54  			Context: c,
    55  		}
    56  
    57  		if caching {
    58  			coll.Coordinator = cc.NewCache(coll.Coordinator, 0, 0)
    59  		}
    60  
    61  		Convey(`Can process multiple single full streams from a Butler bundle.`, func() {
    62  			bb.addFullStream("foo/+/bar", 128)
    63  			bb.addFullStream("foo/+/baz", 256)
    64  
    65  			So(coll.Process(c, bb.bundle()), ShouldBeNil)
    66  
    67  			So(tcc, shouldHaveRegisteredStream, "test-project", "foo/+/bar", 127)
    68  			So(st, shouldHaveStoredStream, "test-project", "foo/+/bar", indexRange{0, 127})
    69  
    70  			So(tcc, shouldHaveRegisteredStream, "test-project", "foo/+/baz", 255)
    71  			So(st, shouldHaveStoredStream, "test-project", "foo/+/baz", indexRange{0, 255})
    72  		})
    73  
    74  		Convey(`Will return a transient error if a transient error happened while registering.`, func() {
    75  			tcc.registerCallback = func(cc.LogStreamState) error { return errors.New("test error", transient.Tag) }
    76  
    77  			bb.addFullStream("foo/+/bar", 128)
    78  			err := coll.Process(c, bb.bundle())
    79  			So(err, ShouldNotBeNil)
    80  			So(transient.Tag.In(err), ShouldBeTrue)
    81  		})
    82  
    83  		Convey(`Will return an error if a non-transient error happened while registering.`, func() {
    84  			tcc.registerCallback = func(cc.LogStreamState) error { return errors.New("test error") }
    85  
    86  			bb.addFullStream("foo/+/bar", 128)
    87  			err := coll.Process(c, bb.bundle())
    88  			So(err, ShouldNotBeNil)
    89  			So(transient.Tag.In(err), ShouldBeFalse)
    90  		})
    91  
    92  		// This will happen when one registration request registers non-terminal,
    93  		// and a follow-on registration request registers with a terminal index. The
    94  		// latter registration request will idempotently succeed, but not accept the
    95  		// terminal index, so termination is still required.
    96  		Convey(`Will terminate a stream if a terminal registration returns a non-terminal response.`, func() {
    97  			terminateCalled := false
    98  			tcc.terminateCallback = func(cc.TerminateRequest) error {
    99  				terminateCalled = true
   100  				return nil
   101  			}
   102  
   103  			bb.addStreamEntries("foo/+/bar", -1, 0, 1)
   104  			So(coll.Process(c, bb.bundle()), ShouldBeNil)
   105  
   106  			bb.addStreamEntries("foo/+/bar", 3, 2, 3)
   107  			So(coll.Process(c, bb.bundle()), ShouldBeNil)
   108  			So(terminateCalled, ShouldBeTrue)
   109  		})
   110  
   111  		Convey(`Will return a transient error if a transient error happened while terminating.`, func() {
   112  			tcc.terminateCallback = func(cc.TerminateRequest) error { return errors.New("test error", transient.Tag) }
   113  
   114  			// Register independently from terminate so we don't bundle RPC.
   115  			bb.addStreamEntries("foo/+/bar", -1, 0, 1, 2, 3, 4)
   116  			So(coll.Process(c, bb.bundle()), ShouldBeNil)
   117  
   118  			// Add terminal index.
   119  			bb.addStreamEntries("foo/+/bar", 5, 5)
   120  			err := coll.Process(c, bb.bundle())
   121  			So(err, ShouldNotBeNil)
   122  			So(transient.Tag.In(err), ShouldBeTrue)
   123  		})
   124  
   125  		Convey(`Will return an error if a non-transient error happened while terminating.`, func() {
   126  			tcc.terminateCallback = func(cc.TerminateRequest) error { return errors.New("test error") }
   127  
   128  			// Register independently from terminate so we don't bundle RPC.
   129  			bb.addStreamEntries("foo/+/bar", -1, 0, 1, 2, 3, 4)
   130  			So(coll.Process(c, bb.bundle()), ShouldBeNil)
   131  
   132  			// Add terminal index.
   133  			bb.addStreamEntries("foo/+/bar", 5, 5)
   134  			err := coll.Process(c, bb.bundle())
   135  			So(err, ShouldNotBeNil)
   136  			So(transient.Tag.In(err), ShouldBeFalse)
   137  		})
   138  
   139  		Convey(`Will return a transient error if a transient error happened on storage.`, func() {
   140  			// Single transient error.
   141  			count := int32(0)
   142  			st.err = func() error {
   143  				if atomic.AddInt32(&count, 1) == 1 {
   144  					return errors.New("test error", transient.Tag)
   145  				}
   146  				return nil
   147  			}
   148  
   149  			bb.addFullStream("foo/+/bar", 128)
   150  			err := coll.Process(c, bb.bundle())
   151  			So(err, ShouldNotBeNil)
   152  			So(transient.Tag.In(err), ShouldBeTrue)
   153  		})
   154  
   155  		Convey(`Will drop invalid LogStreamDescriptor bundle entries and process the valid ones.`, func() {
   156  			be := bb.genBundleEntry("foo/+/trash", 1337, 4, 5, 6, 7, 8)
   157  			bb.addBundleEntry(be)
   158  
   159  			bb.addStreamEntries("foo/+/trash", 0, 1, 3) // Invalid: non-contiguous
   160  			bb.addFullStream("foo/+/bar", 32)
   161  
   162  			err := coll.Process(c, bb.bundle())
   163  			So(err, ShouldNotBeNil)
   164  			So(transient.Tag.In(err), ShouldBeFalse)
   165  
   166  			So(tcc, shouldHaveRegisteredStream, "test-project", "foo/+/bar", 32)
   167  			So(st, shouldHaveStoredStream, "test-project", "foo/+/bar", indexRange{0, 31})
   168  
   169  			So(tcc, shouldHaveRegisteredStream, "test-project", "foo/+/trash", 1337)
   170  			So(st, shouldHaveStoredStream, "test-project", "foo/+/trash", 4, 5, 6, 7, 8)
   171  		})
   172  
   173  		Convey(`Will drop streams with missing (invalid) secrets.`, func() {
   174  			b := bb.genBase()
   175  			b.Secret = nil
   176  			bb.addFullStream("foo/+/bar", 4)
   177  
   178  			err := coll.Process(c, bb.bundle())
   179  			So(err, ShouldErrLike, "invalid prefix secret")
   180  			So(transient.Tag.In(err), ShouldBeFalse)
   181  		})
   182  
   183  		Convey(`Will drop messages with mismatching secrets.`, func() {
   184  			bb.addStreamEntries("foo/+/bar", -1, 0, 1, 2)
   185  			So(coll.Process(c, bb.bundle()), ShouldBeNil)
   186  
   187  			// Push another bundle with a different secret.
   188  			b := bb.genBase()
   189  			b.Secret = bytes.Repeat([]byte{0xAA}, types.PrefixSecretLength)
   190  			be := bb.genBundleEntry("foo/+/bar", 4, 3, 4)
   191  			be.TerminalIndex = 1337
   192  			bb.addBundleEntry(be)
   193  			bb.addFullStream("foo/+/baz", 3)
   194  			So(coll.Process(c, bb.bundle()), ShouldBeNil)
   195  
   196  			So(tcc, shouldHaveRegisteredStream, "test-project", "foo/+/bar", -1)
   197  			So(st, shouldHaveStoredStream, "test-project", "foo/+/bar", indexRange{0, 2})
   198  
   199  			So(tcc, shouldHaveRegisteredStream, "test-project", "foo/+/baz", 2)
   200  			So(st, shouldHaveStoredStream, "test-project", "foo/+/baz", indexRange{0, 2})
   201  		})
   202  
   203  		Convey(`With an empty project name, will drop the stream.`, func() {
   204  			b := bb.genBase()
   205  			b.Project = ""
   206  			bb.addFullStream("foo/+/baz", 3)
   207  
   208  			err := coll.Process(c, bb.bundle())
   209  			So(err, ShouldErrLike, "invalid bundle project name")
   210  			So(transient.Tag.In(err), ShouldBeFalse)
   211  		})
   212  
   213  		Convey(`Will drop streams with invalid project names.`, func() {
   214  			b := bb.genBase()
   215  			b.Project = "!!!invalid name!!!"
   216  			So(config.ValidateProjectName(b.Project), ShouldNotBeNil)
   217  
   218  			err := coll.Process(c, bb.bundle())
   219  			So(err, ShouldErrLike, "invalid bundle project name")
   220  			So(transient.Tag.In(err), ShouldBeFalse)
   221  		})
   222  
   223  		Convey(`Will drop streams with empty bundle prefixes.`, func() {
   224  			b := bb.genBase()
   225  			b.Prefix = ""
   226  
   227  			err := coll.Process(c, bb.bundle())
   228  			So(err, ShouldErrLike, "invalid bundle prefix")
   229  			So(transient.Tag.In(err), ShouldBeFalse)
   230  		})
   231  
   232  		Convey(`Will drop streams with invalid bundle prefixes.`, func() {
   233  			b := bb.genBase()
   234  			b.Prefix = "!!!invalid prefix!!!"
   235  			So(types.StreamName(b.Prefix).Validate(), ShouldNotBeNil)
   236  
   237  			err := coll.Process(c, bb.bundle())
   238  			So(err, ShouldErrLike, "invalid bundle prefix")
   239  			So(transient.Tag.In(err), ShouldBeFalse)
   240  		})
   241  
   242  		Convey(`Will drop streams whose descriptor prefix doesn't match its bundle's prefix.`, func() {
   243  			bb.addStreamEntries("baz/+/bar", 3, 0, 1, 2, 3, 4)
   244  
   245  			err := coll.Process(c, bb.bundle())
   246  			So(err, ShouldErrLike, "mismatched bundle and entry prefixes")
   247  			So(transient.Tag.In(err), ShouldBeFalse)
   248  		})
   249  
   250  		Convey(`Will return no error if the data has a corrupt bundle header.`, func() {
   251  			So(coll.Process(c, []byte{0x00}), ShouldBeNil)
   252  			So(tcc, shouldNotHaveRegisteredStream, "test-project", "foo/+/bar")
   253  		})
   254  
   255  		Convey(`Will drop bundles with unknown ProtoVersion string.`, func() {
   256  			buf := bytes.Buffer{}
   257  			w := pubsubprotocol.Writer{ProtoVersion: "!!!invalid!!!"}
   258  			w.Write(&buf, &logpb.ButlerLogBundle{})
   259  
   260  			So(coll.Process(c, buf.Bytes()), ShouldBeNil)
   261  
   262  			So(tcc, shouldNotHaveRegisteredStream, "test-project", "foo/+/bar")
   263  		})
   264  
   265  		Convey(`Will not ingest records if the stream is archived.`, func() {
   266  			tcc.register(cc.LogStreamState{
   267  				Project:       "test-project",
   268  				Path:          "foo/+/bar",
   269  				Secret:        testSecret,
   270  				TerminalIndex: -1,
   271  				Archived:      true,
   272  			})
   273  
   274  			bb.addStreamEntries("foo/+/bar", 3, 0, 1, 2, 3, 4)
   275  			So(coll.Process(c, bb.bundle()), ShouldBeNil)
   276  
   277  			So(tcc, shouldHaveRegisteredStream, "test-project", "foo/+/bar", -1)
   278  			So(st, shouldHaveStoredStream, "test-project", "foo/+/bar")
   279  		})
   280  
   281  		Convey(`Will not ingest records if the stream is purged.`, func() {
   282  			tcc.register(cc.LogStreamState{
   283  				Project:       "test-project",
   284  				Path:          "foo/+/bar",
   285  				Secret:        testSecret,
   286  				TerminalIndex: -1,
   287  				Purged:        true,
   288  			})
   289  
   290  			So(coll.Process(c, bb.bundle()), ShouldBeNil)
   291  
   292  			So(tcc, shouldHaveRegisteredStream, "test-project", "foo/+/bar", -1)
   293  			So(st, shouldHaveStoredStream, "test-project", "foo/+/bar")
   294  		})
   295  
   296  		Convey(`Will not ingest a bundle with no bundle entries.`, func() {
   297  			So(coll.Process(c, bb.bundle()), ShouldBeNil)
   298  		})
   299  
   300  		Convey(`Will not ingest a bundle whose log entries don't match their descriptor.`, func() {
   301  			be := bb.genBundleEntry("foo/+/bar", 4, 0, 1, 2, 3, 4)
   302  
   303  			// Add a binary log entry. This does NOT match the text descriptor, and
   304  			// should fail validation.
   305  			be.Logs = append(be.Logs, &logpb.LogEntry{
   306  				StreamIndex: 2,
   307  				Sequence:    2,
   308  				Content: &logpb.LogEntry_Binary{
   309  					&logpb.Binary{
   310  						Data: []byte{0xd0, 0x6f, 0x00, 0xd5},
   311  					},
   312  				},
   313  			})
   314  			bb.addBundleEntry(be)
   315  			So(coll.Process(c, bb.bundle()), ShouldErrLike, "invalid log entry")
   316  
   317  			So(tcc, shouldNotHaveRegisteredStream, "test-project", "foo/+/bar")
   318  		})
   319  	})
   320  }
   321  
   322  // TestCollector runs through a series of end-to-end Collector workflows and
   323  // ensures that the Collector behaves appropriately.
   324  func TestCollector(t *testing.T) {
   325  	t.Parallel()
   326  
   327  	testCollectorImpl(t, false)
   328  }
   329  
   330  // TestCollectorWithCaching runs through a series of end-to-end Collector
   331  // workflows and ensures that the Collector behaves appropriately.
   332  func TestCollectorWithCaching(t *testing.T) {
   333  	t.Parallel()
   334  
   335  	testCollectorImpl(t, true)
   336  }