Skip to content

Commit 44acb06

Browse files
yhakbarRahul-Kumar-prog
authored andcommitted
fix: Fixing provider cache test flake (gruntwork-io#6056)
1 parent a65a393 commit 44acb06

3 files changed

Lines changed: 67 additions & 142 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
version: "v1.0.4"
3+
category: "bug-fixes"
4+
---
5+
6+
#### Provider Cache Server now drains pending downloads on shutdown
7+
8+
When Terragrunt finished a run with the Provider Cache Server enabled, the cache server stopped instantly rather than letting in-flight provider downloads complete. Downloads still being served to OpenTofu/Terraform were cut off mid-response, and successful runs ended with a `context canceled` error in the logs.
9+
10+
Terragrunt now waits for those downloads to finish before stopping the cache server, and a graceful shutdown is no longer reported as an error.

internal/providercache/providercache_test.go

Lines changed: 47 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -109,164 +109,72 @@ func TestProviderCache(t *testing.T) {
109109
t.Run(fmt.Sprintf("testCase-%d", i), func(t *testing.T) {
110110
t.Parallel()
111111

112-
// TODO: Remove this once we can invest time in figuring out why this test is so flaky.
113-
//
114-
// It's a pain, but it's not worth the time to fix it.
115-
maxRetries := 3
112+
ctx, cancel := context.WithCancel(t.Context())
113+
defer cancel()
116114

117-
var lastErr error
115+
errGroup, ctx := errgroup.WithContext(ctx)
116+
l := logger.CreateLogger()
118117

119-
for attempt := 1; attempt <= maxRetries; attempt++ {
120-
if attempt > 1 {
121-
t.Logf("Retry attempt %d/%d for test case %d", attempt, maxRetries, i)
122-
}
118+
providerService := services.NewProviderService(providerCacheDir, pluginCacheDir, nil, l)
119+
providerHandler := handlers.NewDirectProviderHandler(l, new(cliconfig.ProviderInstallationDirect), nil)
120+
proxyProviderHandler := handlers.NewProxyProviderHandler(l, nil)
123121

124-
// Create a new context for each test case to avoid interference
125-
//
126-
//nolint:usetesting
127-
ctx := context.Background()
122+
tc.opts = append(tc.opts,
123+
cache.WithProviderService(providerService),
124+
cache.WithProviderHandlers(providerHandler),
125+
cache.WithProxyProviderHandler(proxyProviderHandler),
126+
)
128127

129-
ctx, cancel := context.WithCancel(ctx)
130-
defer cancel()
128+
server := cache.NewServer(tc.opts...)
131129

132-
errGroup, ctx := errgroup.WithContext(ctx)
133-
logger := logger.CreateLogger()
130+
ln, err := server.Listen(t.Context())
131+
require.NoError(t, err)
134132

135-
providerService := services.NewProviderService(providerCacheDir, pluginCacheDir, nil, logger)
136-
providerHandler := handlers.NewDirectProviderHandler(logger, new(cliconfig.ProviderInstallationDirect), nil)
137-
proxyProviderHandler := handlers.NewProxyProviderHandler(logger, nil)
133+
defer ln.Close()
138134

139-
tc.opts = append(tc.opts,
140-
cache.WithProviderService(providerService),
141-
cache.WithProviderHandlers(providerHandler),
142-
cache.WithProxyProviderHandler(proxyProviderHandler),
143-
)
135+
errGroup.Go(func() error {
136+
return server.Run(ctx, ln)
137+
})
144138

145-
server := cache.NewServer(tc.opts...)
139+
urlPath := server.ProviderController.URL()
140+
urlPath.Path += tc.relURLPath
146141

147-
ln, err := server.Listen(t.Context())
148-
if err != nil {
149-
lastErr = err
150-
151-
if attempt < maxRetries {
152-
continue
153-
}
154-
155-
require.NoError(t, err)
156-
}
157-
defer ln.Close()
158-
159-
errGroup.Go(func() error {
160-
return server.Run(ctx, ln)
161-
})
162-
163-
urlPath := server.ProviderController.URL()
164-
urlPath.Path += tc.relURLPath
165-
166-
if tc.fullURLPath != "" {
167-
urlPath.Path = tc.fullURLPath
168-
}
169-
170-
req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlPath.String(), nil)
171-
if err != nil {
172-
lastErr = err
173-
174-
if attempt < maxRetries {
175-
continue
176-
}
177-
178-
require.NoError(t, err)
179-
}
180-
181-
req.Header.Set("Authorization", "Bearer "+token)
182-
183-
resp, err := http.DefaultClient.Do(req)
184-
if err != nil {
185-
lastErr = err
186-
187-
if attempt < maxRetries {
188-
continue
189-
}
190-
191-
require.NoError(t, err)
192-
}
193-
defer resp.Body.Close()
194-
195-
if resp.StatusCode != tc.expectedStatusCode {
196-
lastErr = fmt.Errorf("expected status code %d, got %d", tc.expectedStatusCode, resp.StatusCode)
197-
198-
if attempt < maxRetries {
199-
continue
200-
}
201-
202-
assert.Equal(t, tc.expectedStatusCode, resp.StatusCode)
203-
}
204-
205-
if tc.expectedBodyReg != nil {
206-
body, err := io.ReadAll(resp.Body)
207-
if err != nil {
208-
lastErr = err
209-
210-
if attempt < maxRetries {
211-
continue
212-
}
213-
214-
require.NoError(t, err)
215-
}
216-
217-
if !tc.expectedBodyReg.MatchString(string(body)) {
218-
lastErr = fmt.Errorf("body did not match expected regex: %s", tc.expectedBodyReg.String())
219-
220-
if attempt < maxRetries {
221-
continue
222-
}
223-
224-
assert.Regexp(t, tc.expectedBodyReg, string(body))
225-
}
226-
}
227-
228-
// Skip WaitForCacheReady for unauthorized test cases since they don't trigger background operations,
229-
// and we cancel context at the end of the test.
230-
if tc.expectedStatusCode != http.StatusUnauthorized {
231-
_, err = providerService.WaitForCacheReady("")
232-
if err != nil {
233-
lastErr = err
234-
235-
if attempt < maxRetries {
236-
continue
237-
}
142+
if tc.fullURLPath != "" {
143+
urlPath.Path = tc.fullURLPath
144+
}
238145

239-
require.NoError(t, err)
240-
}
241-
}
146+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlPath.String(), nil)
147+
require.NoError(t, err)
242148

243-
if tc.expectedCachePath != "" {
244-
if !assert.FileExists(t, filepath.Join(providerCacheDir, tc.expectedCachePath)) {
245-
lastErr = fmt.Errorf("expected cache file does not exist: %s", tc.expectedCachePath)
149+
req.Header.Set("Authorization", "Bearer "+token)
246150

247-
if attempt < maxRetries {
248-
continue
249-
}
250-
}
251-
}
151+
resp, err := http.DefaultClient.Do(req)
152+
require.NoError(t, err)
252153

253-
cancel()
154+
defer resp.Body.Close()
254155

255-
err = errGroup.Wait()
256-
if err != nil {
257-
lastErr = err
156+
assert.Equal(t, tc.expectedStatusCode, resp.StatusCode)
258157

259-
if attempt < maxRetries {
260-
continue
261-
}
158+
if tc.expectedBodyReg != nil {
159+
body, err := io.ReadAll(resp.Body)
160+
require.NoError(t, err)
161+
assert.Regexp(t, tc.expectedBodyReg, string(body))
162+
}
262163

263-
require.NoError(t, err)
264-
}
164+
// Skip WaitForCacheReady for unauthorized test cases since they don't trigger background operations,
165+
// and we cancel context at the end of the test.
166+
if tc.expectedStatusCode != http.StatusUnauthorized {
167+
_, err = providerService.WaitForCacheReady("")
168+
require.NoError(t, err)
169+
}
265170

266-
return
171+
if tc.expectedCachePath != "" {
172+
assert.FileExists(t, filepath.Join(providerCacheDir, tc.expectedCachePath))
267173
}
268174

269-
t.Fatalf("Test case %d failed after %d attempts. Last error: %v", i, maxRetries, lastErr)
175+
cancel()
176+
177+
require.NoError(t, errGroup.Wait())
270178
})
271179
}
272180
}

internal/tf/cache/server.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,13 @@ func (server *Server) Run(ctx context.Context, ln net.Listener) error {
103103
<-ctx.Done()
104104
server.logger.Infof("Shutting down Terragrunt Cache server...")
105105

106-
ctx, cancel := context.WithTimeout(ctx, server.shutdownTimeout)
106+
// The parent ctx is by definition already cancelled here; detach from
107+
// its cancellation (preserving any values) so http.Server.Shutdown gets
108+
// the full shutdownTimeout to drain in-flight requests.
109+
shutdownCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), server.shutdownTimeout)
107110
defer cancel()
108111

109-
if err := server.Shutdown(ctx); err != nil {
112+
if err := server.Shutdown(shutdownCtx); err != nil {
110113
return errors.New(err)
111114
}
112115

@@ -119,5 +122,9 @@ func (server *Server) Run(ctx context.Context, ln net.Listener) error {
119122

120123
defer server.logger.Infof("Terragrunt Cache server stopped")
121124

122-
return errGroup.Wait()
125+
if err := errGroup.Wait(); err != nil && !errors.Is(err, context.Canceled) {
126+
return err
127+
}
128+
129+
return nil
123130
}

0 commit comments

Comments
 (0)