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  }