github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/cloud/cloud_status_manager.go (about) 1 package cloud 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "io" 8 "net/http" 9 "sync" 10 "time" 11 12 "github.com/jonboulle/clockwork" 13 14 "github.com/tilt-dev/tilt/internal/cloud/cloudurl" 15 "github.com/tilt-dev/tilt/internal/store" 16 "github.com/tilt-dev/tilt/internal/token" 17 "github.com/tilt-dev/tilt/pkg/logger" 18 "github.com/tilt-dev/tilt/pkg/model" 19 ) 20 21 // to avoid infinitely resubmitting requests on error 22 const timeoutAfterError = 5 * time.Minute 23 24 // how frequently we'll refresh cloud status, even if nothing changes 25 const refreshPeriod = time.Hour 26 27 func NewStatusManager(client HttpClient, clock clockwork.Clock) *CloudStatusManager { 28 return &CloudStatusManager{client: client, clock: clock} 29 } 30 31 // if any of these fields change, we know we need to do a fresh lookup 32 type statusRequestKey struct { 33 tiltToken token.Token 34 teamID string 35 version model.TiltBuild 36 } 37 38 type CloudStatusManager struct { 39 client HttpClient 40 clock clockwork.Clock 41 42 mu sync.Mutex 43 44 lastErrorTime time.Time 45 currentlyMakingRequest bool 46 47 lastRequestKey statusRequestKey 48 lastSuccessfulLookup time.Time 49 } 50 51 func ProvideHttpClient() HttpClient { 52 return http.DefaultClient 53 } 54 55 type HttpClient interface { 56 Do(req *http.Request) (*http.Response, error) 57 } 58 59 type whoAmIResponse struct { 60 SuggestedTiltVersion string 61 } 62 63 func (c *CloudStatusManager) error() { 64 c.mu.Lock() 65 c.lastErrorTime = c.clock.Now() 66 c.mu.Unlock() 67 } 68 69 func (c *CloudStatusManager) CheckStatus(ctx context.Context, st store.RStore, cloudAddress string, requestKey statusRequestKey) { 70 c.mu.Lock() 71 c.currentlyMakingRequest = true 72 c.mu.Unlock() 73 74 defer func() { 75 c.mu.Lock() 76 c.currentlyMakingRequest = false 77 c.mu.Unlock() 78 }() 79 80 u := cloudurl.URL(cloudAddress) 81 u.Path = "/api/whoami" 82 83 body := &bytes.Buffer{} 84 req, err := http.NewRequest("GET", u.String(), body) 85 if err != nil { 86 logger.Get(ctx).Debugf("error making whoami request: %v", err) 87 c.error() 88 return 89 } 90 91 resp, err := c.client.Do(req) 92 if err != nil { 93 logger.Get(ctx).Debugf("error checking tilt cloud status: %v", err) 94 c.error() 95 return 96 } 97 98 if resp.StatusCode != http.StatusOK { 99 body, err := io.ReadAll(resp.Body) 100 if err != nil { 101 logger.Get(ctx).Debugf("tilt cloud status request failed with status %d. error reading response body: %v", resp.StatusCode, err) 102 c.error() 103 return 104 } 105 logger.Get(ctx).Debugf("error checking tilt cloud status: code: %d, message: %s", resp.StatusCode, string(body)) 106 c.error() 107 return 108 } 109 110 responseBody, err := io.ReadAll(resp.Body) 111 if err != nil { 112 logger.Get(ctx).Debugf("error reading response body: %v", err) 113 c.error() 114 return 115 } 116 r := whoAmIResponse{} 117 err = json.NewDecoder(bytes.NewReader(responseBody)).Decode(&r) 118 if err != nil { 119 logger.Get(ctx).Debugf("error decoding tilt whoami response '%s': %v", string(responseBody), err) 120 c.error() 121 return 122 } 123 124 c.mu.Lock() 125 c.lastRequestKey = requestKey 126 c.lastSuccessfulLookup = c.clock.Now() 127 c.lastErrorTime = time.Time{} 128 c.mu.Unlock() 129 130 st.Dispatch(store.TiltCloudStatusReceivedAction{ 131 SuggestedTiltVersion: r.SuggestedTiltVersion, 132 }) 133 } 134 135 func (c *CloudStatusManager) needsLookup(requestKey statusRequestKey) bool { 136 return c.lastSuccessfulLookup.IsZero() || 137 c.lastSuccessfulLookup.Add(refreshPeriod).Before(c.clock.Now()) || 138 requestKey != c.lastRequestKey 139 } 140 141 func (c *CloudStatusManager) OnChange(ctx context.Context, st store.RStore, _ store.ChangeSummary) error { 142 state := st.RLockState() 143 defer st.RUnlockState() 144 145 c.mu.Lock() 146 lastErrorTime := c.lastErrorTime 147 currentlyMakingRequest := c.currentlyMakingRequest 148 requestKey := statusRequestKey{teamID: state.TeamID, tiltToken: state.Token, version: state.TiltBuildInfo} 149 needsLookup := c.needsLookup(requestKey) 150 c.mu.Unlock() 151 152 // c.currentlyMakingRequest is a bit of a race condition here: 153 // 1. start making request that's going to return TokenKnownUnregistered = true 154 // 2. before request finishes, web ui triggers refresh, setting TokenKnownUnregistered = false 155 // 3. request started in (1) finishes, sets TokenKnownUnregistered = true 156 // we never make a request post-(2), where the token was registered 157 // This is mitigated by - a) the window between (1) and (3) is small, and b) the user can just click refresh again 158 allowedToPerformLookup := !time.Now().Before(lastErrorTime.Add(timeoutAfterError)) && !currentlyMakingRequest 159 160 if needsLookup && allowedToPerformLookup { 161 go c.CheckStatus(ctx, st, state.CloudAddress, requestKey) 162 return nil 163 } 164 return nil 165 }