github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/tenants/manager_test.go (about) 1 package tenants 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "testing" 8 "time" 9 10 "github.com/grafana/synthetic-monitoring-agent/internal/cache" 11 sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring" 12 "github.com/rs/zerolog" 13 "github.com/stretchr/testify/require" 14 "google.golang.org/grpc" 15 ) 16 17 type testTenantsClient struct { 18 tenants map[int64]sm.Tenant 19 requestCount map[int64]int 20 err error 21 } 22 23 var errTenantNotFound = errors.New("tenant not found") 24 25 func (c *testTenantsClient) GetTenant(ctx context.Context, in *sm.TenantInfo, opts ...grpc.CallOption) (*sm.Tenant, error) { 26 c.requestCount[in.Id]++ 27 28 if c.err != nil { 29 return nil, c.err 30 } 31 32 tenant, found := c.tenants[in.Id] 33 if !found { 34 return nil, errTenantNotFound 35 } 36 37 return &tenant, nil 38 } 39 40 func makeTenant(idx int) sm.Tenant { 41 return sm.Tenant{ 42 Id: int64(idx), 43 OrgId: int64(idx * 1000), 44 MetricsRemote: &sm.RemoteInfo{ 45 Name: fmt.Sprintf("test-%d", idx), 46 Url: fmt.Sprintf("http://127.0.0.1/%d", idx), 47 Username: fmt.Sprintf("user-%d", idx), 48 Password: fmt.Sprintf("pw-%d", idx), 49 }, 50 } 51 } 52 53 func TestTenantManagerGetTenant(t *testing.T) { 54 tc := testTenantsClient{ 55 tenants: map[int64]sm.Tenant{ 56 1: makeTenant(1), 57 }, 58 requestCount: make(map[int64]int), 59 } 60 61 deadline, hasTimeout := t.Deadline() 62 if !hasTimeout { 63 deadline = time.Now().Add(5 * time.Second) 64 } 65 66 ctx, cancel := context.WithDeadline(context.Background(), deadline) 67 defer cancel() 68 69 tenantCh := make(chan sm.Tenant) 70 cacheExpirationTime := 200 * time.Millisecond 71 maxCacheExpirationTime := cacheExpirationTime + time.Duration(defaultCacheJitter*float64(cacheExpirationTime)) 72 logger := zerolog.New(zerolog.NewTestWriter(t)) 73 74 // Create a local cache for testing 75 // Use DefaultTTL = 0 to disable automatic expiration, allowing us to test 76 // the stale data fallback behavior (ValidUntil is checked manually) 77 localCache, err := cache.NewLocal(cache.LocalConfig{ 78 MaxCapacity: 100, 79 InitialCapacity: 10, 80 DefaultTTL: 0, // Disable automatic expiration 81 Logger: logger, 82 }) 83 require.NoError(t, err) 84 85 defer localCache.Close() 86 87 tm := NewManager(ctx, &tc, tenantCh, cacheExpirationTime, localCache, logger) 88 89 t1 := tc.tenants[1] 90 91 // requesting an existent tenant should return it 92 tenant, err := tm.GetTenant(ctx, &sm.TenantInfo{Id: t1.Id}) 93 require.NoError(t, err) 94 require.NotNil(t, tenant) 95 require.Equal(t, 1, tc.requestCount[t1.Id]) 96 require.Equal(t, t1, *tenant) 97 98 // requesting the same tenant within a short period of time 99 // should not cause a new request to the server, so the count 100 // should remain at 1. 101 tenant, err = tm.GetTenant(ctx, &sm.TenantInfo{Id: t1.Id}) 102 require.NoError(t, err) 103 require.NotNil(t, tenant) 104 require.Equal(t, 1, tc.requestCount[t1.Id]) 105 require.Equal(t, t1, *tenant) 106 107 // requesting the same tenant after a longer time should evict 108 // the existing tenant and make a new request; make sure we are 109 // not getting a cached copy. 110 time.Sleep(maxCacheExpirationTime) 111 112 t1.MetricsRemote.Password += "-new" 113 114 tenant, err = tm.GetTenant(ctx, &sm.TenantInfo{Id: t1.Id}) 115 require.NoError(t, err) 116 require.NotNil(t, tenant) 117 require.Equal(t, 2, tc.requestCount[t1.Id]) 118 require.Equal(t, t1, *tenant) 119 120 // create a new tenant but don't insert it in the cache 121 t2 := makeTenant(2) 122 123 // requesting a non-existent tenant should return an error 124 tenant, err = tm.GetTenant(ctx, &sm.TenantInfo{Id: t2.Id}) 125 require.Error(t, err) 126 require.Equal(t, errTenantNotFound, err) 127 require.Nil(t, tenant) 128 require.Equal(t, 1, tc.requestCount[t2.Id]) 129 130 // negative responses should not be cached 131 tenant, err = tm.GetTenant(ctx, &sm.TenantInfo{Id: t2.Id}) 132 require.Error(t, err) 133 require.Equal(t, errTenantNotFound, err) 134 require.Nil(t, tenant) 135 require.Equal(t, 2, tc.requestCount[t2.Id]) 136 137 // after adding the tenant, a new request for that tenant should 138 // return the correct information 139 tc.tenants[2] = t2 140 tenant, err = tm.GetTenant(ctx, &sm.TenantInfo{Id: t2.Id}) 141 require.NoError(t, err) 142 require.NotNil(t, tenant) 143 require.Equal(t, 3, tc.requestCount[t2.Id]) 144 require.Equal(t, t2, *tenant) 145 146 // create a new tenant and send it over the channel to the 147 // tenant manager 148 t3 := makeTenant(3) 149 150 tenantCh <- t3 151 // Give the manager goroutine some time to process the channel message 152 // before we start checking. This reduces unnecessary API calls. 153 time.Sleep(10 * time.Millisecond) 154 155 // Now check if the tenant was added to the cache via the channel 156 // by verifying that GetTenant succeeds without making an API request 157 tenant, err = tm.GetTenant(ctx, &sm.TenantInfo{Id: t3.Id}) 158 require.NoError(t, err) 159 require.NotNil(t, tenant) 160 require.Equal(t, 0, tc.requestCount[t3.Id], "should not have made API request for tenant received via channel") 161 require.Equal(t, t3, *tenant) 162 163 // wait for tenants to expire 164 time.Sleep(maxCacheExpirationTime) 165 // force tenants client to return an error 166 tc.err = errors.New("network error") 167 168 // if a tenant is present in the cache, this one should be returned, even 169 // if it is expired, in case new data can not be retrieved from the API 170 tenant, err = tm.GetTenant(ctx, &sm.TenantInfo{Id: t3.Id}) 171 require.NoError(t, err) 172 require.NotNil(t, tenant) 173 require.Equal(t, 1, tc.requestCount[t3.Id]) 174 require.Equal(t, t3, *tenant) 175 } 176 177 func TestCalculateValidUntil(t *testing.T) { 178 var ( 179 now = time.Now() 180 // compute the relative error if there's a 1 second difference 181 // between the time we expect (which is around now+timeout) and 182 // a time that is 1 second later. In other words, tolare a 183 // relative difference of about 1 second in the results we get. 184 defEpsilon = float64(now.Add(1*time.Second).UnixNano()-now.UnixNano()) / float64(now.UnixNano()) 185 // For cases on which the secret store expiraiton is not taken into 186 // account, a jitter is applied to the timeout. 187 jitterEpsilon = defaultCacheJitter 188 ) 189 190 // Some tests below assume this is true. 191 require.Greater(t, sm.MaxScriptedTimeout, 2*time.Minute) 192 require.Less(t, sm.MaxScriptedTimeout, 5*time.Minute) 193 194 testCases := map[string]struct { 195 timeout time.Duration 196 tenant *sm.Tenant 197 want time.Duration 198 epsilon float64 199 }{ 200 "10 minute timeout, no secret store": { 201 timeout: 10 * time.Minute, 202 tenant: &sm.Tenant{}, 203 want: 10 * time.Minute, 204 epsilon: jitterEpsilon, 205 }, 206 "1 hour timeout, no secret store": { 207 timeout: 1 * time.Hour, 208 tenant: &sm.Tenant{}, 209 want: 1 * time.Hour, 210 epsilon: jitterEpsilon, 211 }, 212 "7.5 minute timeout, secret store expires in 2 minutes (less than MaxScriptedTimeout)": { 213 timeout: 7*time.Minute + 30*time.Second, 214 tenant: &sm.Tenant{ 215 SecretStore: &sm.SecretStore{ 216 Url: "https://secrets.example.com", 217 Token: "token", 218 Expiry: float64(now.Add(2*time.Minute).UnixNano()) / 1e9, 219 }, 220 }, 221 want: 2 * time.Minute, 222 epsilon: defEpsilon, 223 }, 224 "7.5 minute timeout, secret store expires in 5 minutes (more than MaxScriptedTimeout)": { 225 timeout: 7*time.Minute + 30*time.Second, 226 tenant: &sm.Tenant{ 227 SecretStore: &sm.SecretStore{ 228 Url: "https://secrets.example.com", 229 Token: "token", 230 Expiry: float64(now.Add(5*time.Minute).UnixNano()) / 1e9, 231 }, 232 }, 233 want: 5*time.Minute - sm.MaxScriptedTimeout, 234 epsilon: defEpsilon, 235 }, 236 "7.5 minute timeout, secret store expires in 1 hour": { 237 timeout: 7*time.Minute + 30*time.Second, 238 tenant: &sm.Tenant{ 239 SecretStore: &sm.SecretStore{ 240 Url: "https://secrets.example.com", 241 Token: "token", 242 Expiry: float64(now.Add(1*time.Hour).UnixNano()) / 1e9, 243 }, 244 }, 245 want: 7*time.Minute + 30*time.Second, 246 epsilon: defEpsilon, 247 }, 248 // This should not make a difference wrt to the previous tests. Make sure that's the case. 249 "10 minute timeout, secret store expires in 2 minutes": { 250 timeout: 10 * time.Minute, 251 tenant: &sm.Tenant{ 252 SecretStore: &sm.SecretStore{ 253 Url: "https://secrets.example.com", 254 Token: "token", 255 Expiry: float64(now.Add(2*time.Minute).UnixNano()) / 1e9, 256 }, 257 }, 258 want: 2 * time.Minute, 259 epsilon: defEpsilon, 260 }, 261 "10 minute timeout, secret store expires in 5 minutes": { 262 timeout: 10 * time.Minute, 263 tenant: &sm.Tenant{ 264 SecretStore: &sm.SecretStore{ 265 Url: "https://secrets.example.com", 266 Token: "token", 267 Expiry: float64(now.Add(5*time.Minute).UnixNano()) / 1e9, 268 }, 269 }, 270 want: 5*time.Minute - sm.MaxScriptedTimeout, 271 epsilon: defEpsilon, 272 }, 273 "10 minute timeout, secret store expires in 10 minutes": { 274 timeout: 10 * time.Minute, 275 tenant: &sm.Tenant{ 276 SecretStore: &sm.SecretStore{ 277 Url: "https://secrets.example.com", 278 Token: "token", 279 Expiry: float64(now.Add(10*time.Minute).UnixNano()) / 1e9, 280 }, 281 }, 282 want: 10*time.Minute - sm.MaxScriptedTimeout, 283 epsilon: defEpsilon, 284 }, 285 "10 minute timeout, secret store expires in 1 hour": { 286 timeout: 10 * time.Minute, 287 tenant: &sm.Tenant{ 288 SecretStore: &sm.SecretStore{ 289 Url: "https://secrets.example.com", 290 Token: "token", 291 Expiry: float64(now.Add(1*time.Hour).UnixNano()) / 1e9, 292 }, 293 }, 294 want: 10 * time.Minute, 295 epsilon: defEpsilon, 296 }, 297 "10 minute timeout, secret store with empty URL (not configured)": { 298 timeout: 10 * time.Minute, 299 tenant: &sm.Tenant{ 300 SecretStore: &sm.SecretStore{ 301 Url: "", 302 Token: "token", 303 Expiry: float64(now.Add(1*time.Hour).UnixNano()) / 1e9, 304 }, 305 }, 306 want: 0, // Cache should be invalid immediately 307 epsilon: defEpsilon, 308 }, 309 "10 minute timeout, secret store with empty token (not configured)": { 310 timeout: 10 * time.Minute, 311 tenant: &sm.Tenant{ 312 SecretStore: &sm.SecretStore{ 313 Url: "https://secrets.example.com", 314 Token: "", 315 Expiry: float64(now.Add(1*time.Hour).UnixNano()) / 1e9, 316 }, 317 }, 318 want: 0, // Cache should be invalid immediately 319 epsilon: defEpsilon, 320 }, 321 "10 minute timeout, secret store with both URL and token empty (not configured)": { 322 timeout: 10 * time.Minute, 323 tenant: &sm.Tenant{ 324 SecretStore: &sm.SecretStore{ 325 Url: "", 326 Token: "", 327 Expiry: float64(now.Add(1*time.Hour).UnixNano()) / 1e9, 328 }, 329 }, 330 want: 0, // Cache should be invalid immediately 331 epsilon: defEpsilon, 332 }, 333 } 334 335 for name, tc := range testCases { 336 t.Run(name, func(t *testing.T) { 337 tm := &Manager{ 338 timeout: tc.timeout, 339 } 340 341 expected := time.Now().Add(tc.want) 342 actual := tm.calculateValidUntil(tc.tenant) 343 344 require.InEpsilonf(t, expected.UnixNano(), actual.UnixNano(), tc.epsilon, 345 "calculateValidUntil() should be within range. expected: %d, actual: %d, delta: %d", 346 expected.Sub(now).Milliseconds(), 347 actual.Sub(now).Milliseconds(), 348 expected.UnixMilli()-actual.UnixMilli(), 349 ) 350 }) 351 } 352 }