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 }