go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/server/collector/coordinator/cache_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 coordinator 16 17 import ( 18 "context" 19 "fmt" 20 "sync" 21 "sync/atomic" 22 "testing" 23 "time" 24 25 "go.chromium.org/luci/common/clock/testclock" 26 "go.chromium.org/luci/common/errors" 27 "go.chromium.org/luci/common/retry/transient" 28 "go.chromium.org/luci/logdog/common/types" 29 30 . "github.com/smartystreets/goconvey/convey" 31 ) 32 33 // testCoordinator is an implementation of Coordinator that can be used for 34 // testing. 35 type testCoordinator struct { 36 // calls is the number of calls made to the interface's methods. 37 calls int32 38 // callC, if not nil, will have a token pushed to it when a call is made. 39 callC chan struct{} 40 // errC is a channel that error status will be read from if not nil. 41 errC chan error 42 } 43 44 func (c *testCoordinator) RegisterStream(ctx context.Context, s *LogStreamState, desc []byte) ( 45 *LogStreamState, error) { 46 if err := c.incCalls(); err != nil { 47 return nil, err 48 } 49 50 // Set the ProtoVersion to differentiate the output State from the input. 51 rs := *s 52 rs.ProtoVersion = "remote" 53 return &rs, nil 54 } 55 56 func (c *testCoordinator) TerminateStream(ctx context.Context, tr *TerminateRequest) error { 57 if err := c.incCalls(); err != nil { 58 return err 59 } 60 return nil 61 } 62 63 // incCalls is an entry point for client goroutines. It offers the opportunity 64 // to track call count as well as trap executing goroutines within client calls. 65 // 66 // This must not be called while the lock is held, else it could lead to 67 // deadlock if multiple goroutines are trapped. 68 func (c *testCoordinator) incCalls() error { 69 if c.callC != nil { 70 c.callC <- struct{}{} 71 } 72 73 atomic.AddInt32(&c.calls, 1) 74 75 if c.errC != nil { 76 return <-c.errC 77 } 78 return nil 79 } 80 81 func TestStreamStateCache(t *testing.T) { 82 t.Parallel() 83 84 Convey(`Using a test configuration`, t, func() { 85 c, tc := testclock.UseTime(context.Background(), testclock.TestTimeLocal) 86 tcc := testCoordinator{} 87 88 st := LogStreamState{ 89 Project: "test-project", 90 Path: "test-stream-path", 91 ID: "hash12345", 92 TerminalIndex: -1, 93 } 94 95 tr := TerminateRequest{ 96 Project: st.Project, 97 Path: st.Path, 98 ID: st.ID, 99 TerminalIndex: 1337, 100 Secret: st.Secret, 101 } 102 103 // Note: In all of these tests, we check if "proto" field (ProtoVersion) 104 // is "remote". We use ProtoVersion as a channel between our fake remote 105 // service. When our fake remote service returns a LogStreamState, it sets 106 // "remote" to true to differentiate it from the local pushed state. 107 // 108 // If a LogStreamState has "remote" set to true, that implies that it was 109 // sent by the fake testing service rather than the local test. 110 Convey(`A streamStateCache`, func() { 111 ssc := NewCache(&tcc, 4, 1*time.Second) 112 113 resultC := make(chan *LogStreamState) 114 req := func(st *LogStreamState) { 115 var res *LogStreamState 116 defer func() { 117 resultC <- res 118 }() 119 120 st, err := ssc.RegisterStream(c, st, nil) 121 if err == nil { 122 res = st 123 } 124 } 125 126 Convey(`Can register a stream`, func() { 127 s, err := ssc.RegisterStream(c, &st, nil) 128 So(err, ShouldBeNil) 129 So(s.ProtoVersion, ShouldEqual, "remote") 130 So(tcc.calls, ShouldEqual, 1) 131 132 Convey(`Will not re-register the same stream.`, func() { 133 st.ProtoVersion = "" 134 135 s, err := ssc.RegisterStream(c, &st, nil) 136 So(err, ShouldBeNil) 137 So(s.ProtoVersion, ShouldEqual, "remote") 138 So(tcc.calls, ShouldEqual, 1) 139 }) 140 141 Convey(`When the registration expires`, func() { 142 st.ProtoVersion = "" 143 tc.Add(time.Second) 144 145 Convey(`Will re-register the stream.`, func() { 146 s, err := ssc.RegisterStream(c, &st, nil) 147 So(err, ShouldBeNil) 148 So(s.ProtoVersion, ShouldEqual, "remote") 149 So(tcc.calls, ShouldEqual, 2) 150 }) 151 }) 152 153 Convey(`Can terminate a registered stream`, func() { 154 So(ssc.TerminateStream(c, &tr), ShouldBeNil) 155 So(tcc.calls, ShouldEqual, 2) // +1 call 156 157 Convey(`Registering the stream will include the terminal index.`, func() { 158 // Fill it in with junk to make sure we are getting cached. 159 st.TerminalIndex = 123 160 st.ProtoVersion = "" 161 162 s, err := ssc.RegisterStream(c, &st, nil) 163 So(err, ShouldBeNil) 164 So(s.ProtoVersion, ShouldEqual, "remote") 165 So(s.TerminalIndex, ShouldEqual, 1337) 166 So(tcc.calls, ShouldEqual, 2) // No additional calls. 167 }) 168 }) 169 }) 170 171 Convey(`Can register a stream with a terminal index`, func() { 172 st.TerminalIndex = 1337 173 174 s, err := ssc.RegisterStream(c, &st, nil) 175 So(err, ShouldBeNil) 176 So(s.ProtoVersion, ShouldEqual, "remote") 177 So(tcc.calls, ShouldEqual, 1) 178 179 Convey(`A subsequent call to TerminateStream will be ignored, since we have remote terminal confirmation.`, func() { 180 tr.TerminalIndex = 12345 181 182 So(ssc.TerminateStream(c, &tr), ShouldBeNil) 183 So(tcc.calls, ShouldEqual, 1) // (No additional calls) 184 185 Convey(`A register stream call will return the confirmed terminal index.`, func() { 186 st.TerminalIndex = 0 187 188 s, err := ssc.RegisterStream(c, &st, nil) 189 So(err, ShouldBeNil) 190 So(s.ProtoVersion, ShouldEqual, "remote") 191 So(tcc.calls, ShouldEqual, 1) // (No additional calls) 192 So(s.TerminalIndex, ShouldEqual, 1337) 193 }) 194 }) 195 196 Convey(`A subsqeuent register stream call will return the confirmed terminal index.`, func() { 197 st.TerminalIndex = 0 198 199 s, err := ssc.RegisterStream(c, &st, nil) 200 So(err, ShouldBeNil) 201 So(s.ProtoVersion, ShouldEqual, "remote") 202 So(tcc.calls, ShouldEqual, 1) // (No additional calls) 203 So(s.TerminalIndex, ShouldEqual, 1337) 204 }) 205 }) 206 207 Convey(`When multiple goroutines register the same stream, it gets registered once.`, func() { 208 tcc.callC = make(chan struct{}) 209 tcc.errC = make(chan error) 210 211 errs := make(errors.MultiError, 256) 212 for i := 0; i < len(errs); i++ { 213 go req(&st) 214 } 215 216 <-tcc.callC 217 tcc.errC <- nil 218 for i := 0; i < len(errs); i++ { 219 <-resultC 220 } 221 222 So(errors.SingleError(errs), ShouldBeNil) 223 So(tcc.calls, ShouldEqual, 1) 224 }) 225 226 Convey(`Multiple registrations for the same stream will result in two requests if the first expires.`, func() { 227 tcc.callC = make(chan struct{}) 228 tcc.errC = make(chan error) 229 230 // First request. 231 go req(&st) 232 233 // Wait for the request to happen, then advance time past the request's 234 // expiration. 235 <-tcc.callC 236 tc.Add(time.Second) 237 238 // Second request. 239 go req(&st) 240 241 // Release both calls and reap the results. 242 <-tcc.callC 243 tcc.errC <- nil 244 tcc.errC <- nil 245 246 r1 := <-resultC 247 r2 := <-resultC 248 249 So(r1.ProtoVersion, ShouldEqual, "remote") 250 So(r2.ProtoVersion, ShouldEqual, "remote") 251 So(tcc.calls, ShouldEqual, 2) 252 }) 253 254 Convey(`RegisterStream`, func() { 255 Convey(`A transient registration error will result in a RegisterStream error.`, func() { 256 tcc.errC = make(chan error, 1) 257 tcc.errC <- errors.New("test error", transient.Tag) 258 259 _, err := ssc.RegisterStream(c, &st, nil) 260 So(err, ShouldNotBeNil) 261 So(tcc.calls, ShouldEqual, 1) 262 263 Convey(`A second request will call through, try again, and succeed.`, func() { 264 tcc.errC = nil 265 266 _, err := ssc.RegisterStream(c, &st, nil) 267 So(err, ShouldBeNil) 268 So(tcc.calls, ShouldEqual, 2) 269 }) 270 }) 271 272 Convey(`A non-transient registration error will result in a RegisterStream error.`, func() { 273 tcc.errC = make(chan error, 1) 274 tcc.errC <- errors.New("test error") 275 276 _, err := ssc.RegisterStream(c, &st, nil) 277 So(err, ShouldNotBeNil) 278 So(tcc.calls, ShouldEqual, 1) 279 280 Convey(`A second request will return the cached error.`, func() { 281 tcc.errC = nil 282 283 _, err := ssc.RegisterStream(c, &st, nil) 284 So(err, ShouldNotBeNil) 285 So(tcc.calls, ShouldEqual, 1) 286 }) 287 }) 288 }) 289 290 Convey(`TerminateStream`, func() { 291 tr := TerminateRequest{ 292 Project: st.Project, 293 ID: st.ID, 294 TerminalIndex: 1337, 295 } 296 297 Convey(`The termination endpoint returns a transient error, it will propagate.`, func() { 298 tcc.errC = make(chan error, 1) 299 tcc.errC <- errors.New("test error", transient.Tag) 300 301 err := ssc.TerminateStream(c, &tr) 302 So(transient.Tag.In(err), ShouldBeTrue) 303 So(tcc.calls, ShouldEqual, 1) 304 305 Convey(`A second attempt will call through, try again, and succeed.`, func() { 306 tcc.errC = nil 307 308 err := ssc.TerminateStream(c, &tr) 309 So(err, ShouldBeNil) 310 So(tcc.calls, ShouldEqual, 2) 311 }) 312 }) 313 314 Convey(`When the termination endpoint returns a non-transient error, it will propagate.`, func() { 315 tcc.errC = make(chan error, 1) 316 tcc.errC <- errors.New("test error") 317 318 err := ssc.TerminateStream(c, &tr) 319 So(err, ShouldNotBeNil) 320 So(tcc.calls, ShouldEqual, 1) 321 322 Convey(`A second request will return the cached error.`, func() { 323 tcc.errC = nil 324 325 err := ssc.TerminateStream(c, &tr) 326 So(err, ShouldNotBeNil) 327 So(tcc.calls, ShouldEqual, 1) 328 }) 329 }) 330 }) 331 332 Convey(`Different projects with the same stream name will not conflict.`, func() { 333 var projects = []string{"", "foo", "bar"} 334 335 for i, p := range projects { 336 st.Project = p 337 s, err := ssc.RegisterStream(c, &st, nil) 338 So(err, ShouldBeNil) 339 340 So(ssc.TerminateStream(c, &TerminateRequest{ 341 Project: s.Project, 342 Path: s.Path, 343 ID: s.ID, 344 TerminalIndex: types.MessageIndex(i), 345 }), ShouldBeNil) 346 } 347 So(tcc.calls, ShouldEqual, len(projects)*2) 348 349 for i, p := range projects { 350 st.Project = p 351 st.TerminalIndex = -1 352 353 s, err := ssc.RegisterStream(c, &st, nil) 354 So(err, ShouldBeNil) 355 So(s.TerminalIndex, ShouldEqual, types.MessageIndex(i)) 356 } 357 So(tcc.calls, ShouldEqual, len(projects)*2) 358 }) 359 }) 360 361 Convey(`A streamStateCache can register multiple streams at once.`, func() { 362 ssc := NewCache(&tcc, 0, 0) 363 tcc.callC = make(chan struct{}) 364 tcc.errC = make(chan error) 365 366 count := 2048 367 wg := sync.WaitGroup{} 368 errs := make(errors.MultiError, count) 369 state := make([]*LogStreamState, count) 370 wg.Add(count) 371 for i := 0; i < count; i++ { 372 st := st 373 st.Path = types.StreamPath(fmt.Sprintf("ID:%d", i)) 374 375 go func(i int) { 376 defer wg.Done() 377 state[i], errs[i] = ssc.RegisterStream(c, &st, nil) 378 }(i) 379 } 380 381 // Wait for all of them to simultaneously call. 382 for i := 0; i < count; i++ { 383 <-tcc.callC 384 } 385 386 // They're all blocked on errC; allow them to continue. 387 for i := 0; i < count; i++ { 388 tcc.errC <- nil 389 } 390 391 // Wait for them to finish. 392 wg.Wait() 393 394 // Confirm that all registered successfully. 395 So(errors.SingleError(errs), ShouldBeNil) 396 397 remotes := 0 398 for i := 0; i < count; i++ { 399 if state[i].ProtoVersion == "remote" { 400 remotes++ 401 } 402 } 403 So(remotes, ShouldEqual, count) 404 }) 405 }) 406 }