Atlas › Test

TestRequire_WaitsForInFlightAttemptOnTimeout

Exact test identity: go.temporal.io/server/common/testing/await/TestRequire_WaitsForInFlightAttemptOnTimeout

Package
go.temporal.io/server/common/testing/await
Suite / test hierarchy
TestRequire_WaitsForInFlightAttemptOnTimeout
Test
TestRequire_WaitsForInFlightAttemptOnTimeout
Introduced at
cancels_attempt_context_on_timeout, TestRequire_WaitsForInFlightAttemptOnTimeout, +1 Frontier kind: Test frontier
Covered ranges
53
Covered lines
190
Covered files
5

Co-introduced tests

2 other tests enter at the same concept.

Covered source

Expand a file to inspect source; the > gutter marks covered lines.

go.temporal.io/server/common/testing/await/require_ctx.go 81 covered LOC · 27 ranges

Open complete file

20 const defaultSoftDeadlockTimeout = 30 * time.Second
21
22 > func softDeadlockTimeout() time.Duration { require_ctx.go
23 > if s := os.Getenv(softDeadlockTimeoutEnvVar); s != "" {
24 if d, err := time.ParseDuration(s); err == nil {
25 return d
26 }
27 }
28 > return defaultSoftDeadlockTimeout require_ctx.go
29 }
30
40 const defaultHardDeadlockTimeout = 10 * time.Second
41
42 > func hardDeadlockTimeout() time.Duration { require_ctx.go
43 > if s := os.Getenv(hardDeadlockTimeoutEnvVar); s != "" {
44 if d, err := time.ParseDuration(s); err == nil {
45 return d
46 }
47 }
48 > return defaultHardDeadlockTimeout require_ctx.go
49 }
50
54 // Pass the *await.T to require.*/assert.* — failures cause a retry, not a
55 // test failure. Use t.Context() inside the callback to honor the timeout.
56 > func Require(ctx context.Context, tb testing.TB, condition func(*T), timeout, pollInterval time.Duration) { require_ctx.go
57 > tb.Helper()
58 > run(ctx, tb, condition, legacyConfig(timeout, pollInterval, ""), "Require", requireMisuseHint, true)
59 > }
60
61 // Requiref is like [Require] but adds a formatted message to the timeout
74 misuseHint string,
75 cancellable bool,
76 > ) { require_ctx.go
77 > tb.Helper()
78 >
79 > // Skip if the test already failed — no point polling.
80 > if tb.Failed() {
81 tb.Logf("%s: skipping (test already failed)", funcName)
82 return
83 }
84 // Guard: context.WithDeadline panics on a nil parent.
85 > if parentCtx == nil { require_ctx.go
86 tb.Fatalf("%s: nil context", funcName)
87 return
88 }
89
90 > deadline := time.Now().Add(cfg.totalTimeout) require_ctx.go
91 >
92 > // Cap at the parent context's deadline if it's earlier than our timeout.
93 > if parentDeadline, hasDeadline := parentCtx.Deadline(); hasDeadline && parentDeadline.Before(deadline) {
94 deadline = parentDeadline
95 }
97 // Cap at the test's deadline if it's earlier than our deadline.
98 // Ideally, the parent context already accounts for the test's deadline - but we are being defensive.
99 > if d, ok := tb.(interface{ Deadline() (time.Time, bool) }); ok { require_ctx.go
100 if testDeadline, hasDeadline := d.Deadline(); hasDeadline && testDeadline.Before(deadline) {
101 deadline = testDeadline
103 }
104
105 > effectiveTimeout := max(0, time.Until(deadline)) require_ctx.go
106 > awaitCtx, awaitCancel := context.WithDeadline(parentCtx, deadline)
107 > defer awaitCancel()
108 >
109 > report := timeoutReport{effectiveTimeout: effectiveTimeout}
110 >
111 > for {
112 > // Parent context was canceled while we were sleeping (not our deadline).
113 > if err := awaitCtx.Err(); err != nil && !deadlineReached(deadline) {
114 report.reportAttemptErrors(tb)
115 tb.Fatalf("%s: context canceled before condition was satisfied: %v", funcName, err)
117 }
118
119 > report.nextPoll() require_ctx.go
120 >
121 > // Per-attempt context: bounded by the configured attempt timeout and
122 > // further capped by the overall awaitCtx.
123 > attemptCtx, attemptCancel := context.WithTimeout(awaitCtx, cfg.attemptTimeout)
124 > t := &T{tb: tb, ctx: attemptCtx}
125 >
126 > // Run attempt.
127 > res := runAttempt(t, condition, attemptCancel, funcName, cancellable)
128 > attemptCancel()
129 > if res.panicVal != nil {
130 panic(res.panicVal) // propagate to caller
131 }
132 > if res.deadlocked { require_ctx.go
133 report.reportAttemptErrors(tb)
134 if cancellable {
141 return
142 }
143 > report.recordErrors(t.errors) require_ctx.go
144 >
145 > // Attempt-timeout expiry: attemptCtx is done but awaitCtx is not.
146 > // Record nothing special - the attempt's recorded errors (if any)
147 > // already describe what went wrong; otherwise we just retry.
148 > attemptHitOwnTimeout := attemptCtx.Err() == context.DeadlineExceeded && awaitCtx.Err() == nil
149 > if attemptHitOwnTimeout {
150 report.recordAttemptTimeout()
151 }
152
153 // Check misuse where the real test failed instead of just the attempt.
154 > if tb.Failed() { require_ctx.go
155 tb.Fatalf("%s: the test was marked failed directly — %s", funcName, misuseHint)
156 return
158
159 // Parent context was canceled during the attempt (not our deadline).
160 > if err := awaitCtx.Err(); err != nil && !deadlineReached(deadline) { require_ctx.go
161 report.reportAttemptErrors(tb)
162 tb.Fatalf("%s: context canceled before condition was satisfied: %v", funcName, err)
165
166 // Our deadline expired.
167 > if deadlineReached(deadline) { require_ctx.go
168 > report.reportTimeout(tb, funcName, cfg.timeoutMsg) require_ctx.go
169 > return
170 > }
171
172 // Success: attempt completed without failures.
211 funcName string,
212 cancellable bool,
213 > ) attemptResult { require_ctx.go
214 > done := make(chan attemptResult, 1)
215 >
216 > go func() {
217 > completed := false
218 > defer func() {
219 > if r := recover(); r != nil { require_ctx.go
220 if _, ok := r.(attemptFailed); ok {
221 done <- attemptResult{stopped: true}
227 // recover returned nil: either normal return (completed=true) or
228 // runtime.Goexit (completed=false; Goexit is not a panic).
229 > done <- attemptResult{stopped: !completed} require_ctx.go
230 }()
231 > condition(t) require_ctx.go
232 > completed = true
233 }()
234
235 > if cancellable { require_ctx.go
236 > // Soft phase: wait for the condition, our soft timer, or parent cancel. require_ctx.go
237 > softTimer := time.NewTimer(softDeadlockTimeout())
238 > defer softTimer.Stop()
239 >
240 > select {
241 case r := <-done:
242 return r
248 // Cancel so the condition can observe ctx.Done().
249 cancel()
250 > case <-t.ctx.Done(): require_ctx.go
251 // Parent cancelled (await deadline reached or upstream cancel).
252 // Proceed to hard phase quietly.
255
256 // Hard phase: wait for the condition or the hard timer.
257 > hardTimer := time.NewTimer(hardDeadlockTimeout()) require_ctx.go
258 > defer hardTimer.Stop()
259 >
260 > select {
261 > case r := <-done: require_ctx.go
262 > return r
263 case <-hardTimer.C:
264 return attemptResult{deadlocked: true}
281 }
282
283 > func deadlineReached(deadline time.Time) bool { require_ctx.go
284 > return !time.Now().Before(deadline)
285 > }
go.temporal.io/server/common/testing/testcontext/context.go 70 covered LOC · 15 ranges

Open complete file

39
40 // DefaultTimeout returns the effective default timeout for test-scoped contexts.
41 > func DefaultTimeout() time.Duration { context.go
42 > return effectiveTimeout(0)
43 > }
44
45 // For returns the test-scoped context for tb. The context is canceled
49 // return the same context, but an explicit different timeout fails instead of
50 // being silently ignored.
51 > func For(tb testing.TB, opts ...Option) context.Context { context.go
52 > tb.Helper()
53 >
54 > cfg := config{timeout: DefaultTimeout()}
55 > for _, opt := range opts {
56 opt(&cfg)
57 }
58
59 > st := getContextState(tb, cfg.timeout) context.go
60 > st.configure(tb, cfg)
61 > return st.context()
62 }
63
98 }
99
100 > func getContextState(tb testing.TB, timeout time.Duration) *contextState { context.go
101 > tb.Helper()
102 >
103 > testContexts.Lock()
104 > defer testContexts.Unlock()
105 >
106 > if st, ok := testContexts.byTest[tb]; ok {
107 return st
108 }
109
110 > ctx, cancel := context.WithTimeout(tb.Context(), timeout) context.go
111 >
112 > // Annotate gRPC requests with the test name for OTEL tracing.
113 > ctx = metadata.AppendToOutgoingContext(ctx, testNameMetadataKey, tb.Name())
114 >
115 > st := &contextState{
116 > ctx: ctx,
117 > cancel: cancel,
118 > timeout: timeout,
119 > decorators: make(map[any]struct{}),
120 > }
121 > testContexts.byTest[tb] = st
122 >
123 > tb.Cleanup(func() {
124 > err := st.err()
125 > st.cancel()
126 > testContexts.Lock()
127 > delete(testContexts.byTest, tb)
128 > testContexts.Unlock()
129 > if err == context.DeadlineExceeded {
130 tb.Errorf("test exceeded timeout of %v", st.timeout)
131 }
132 > st.release() context.go
133 })
134 > return st context.go
135 }
136
137 > func (s *contextState) configure(tb testing.TB, cfg config) { context.go
138 > tb.Helper()
139 >
140 > s.mu.Lock()
141 > defer s.mu.Unlock()
142 >
143 > if cfg.timeoutSet && cfg.timeout != s.timeout {
144 tb.Fatalf("testcontext: test context already exists with timeout %v; cannot change it to %v", s.timeout, cfg.timeout)
145 }
168 }
169
170 > func (s *contextState) context() context.Context { context.go
171 > s.mu.Lock()
172 > defer s.mu.Unlock()
173 > return s.ctx
174 > }
175
176 > func (s *contextState) err() error { context.go
177 > s.mu.Lock()
178 > defer s.mu.Unlock()
179 > return s.ctx.Err()
180 > }
181
182 > func (s *contextState) release() { context.go
183 > s.mu.Lock()
184 > defer s.mu.Unlock()
185 > s.ctx = nil
186 > }
187
188 > func effectiveTimeout(customTimeout time.Duration) (timeout time.Duration) { context.go
189 > defer func() {
190 > // Build flag TEMPORAL_DEBUG applies a timeout multiplier to all test timeouts.
191 > timeout *= debug.TimeoutMultiplier
192 > }()
193
194 // 1. Custom timeout (via WithTimeout option).
195 > if customTimeout > 0 { context.go
196 return customTimeout
197 }
198
199 // 2. TEMPORAL_TEST_TIMEOUT environment variable.
200 > if envTimeout := os.Getenv("TEMPORAL_TEST_TIMEOUT"); envTimeout != "" { context.go
201 if dur, err := time.ParseDuration(envTimeout); err == nil && dur > 0 {
202 return dur
205
206 // 3. Default timeout.
207 > return defaultTimeout context.go
208 }
go.temporal.io/server/common/testing/await/report.go 18 covered LOC · 7 ranges

Open complete file

28 }
29
30 > func (r *timeoutReport) nextPoll() { report.go
31 > r.attempts++
32 > }
33
34 > func (r *timeoutReport) recordErrors(errors []string) { report.go
35 > if len(errors) > 0 {
36 r.failures = append(r.failures, attemptFailure{attempt: r.attempts, errors: errors})
37 }
42 }
43
44 > func (r timeoutReport) reportAttemptErrors(tb testing.TB) { report.go
45 > reportAttemptErrors(tb, r.failures)
46 > }
47
48 > func (r timeoutReport) reportTimeout(tb testing.TB, funcName, timeoutMsg string) { report.go
49 > r.reportAttemptErrors(tb)
50 > message := fmt.Sprintf("condition not satisfied after %v", r.effectiveTimeout)
51 > if timeoutMsg != "" {
52 message = fmt.Sprintf("%s (not satisfied after %v)", timeoutMsg, r.effectiveTimeout)
53 }
54 > tb.Fatalf("%s: %s\ndetails:\n attempts = %d\n attempt timeouts = %d", report.go
55 > funcName, message, r.attempts, r.attemptTimeouts)
56 }
57
58 > func reportAttemptErrors(tb testing.TB, failures []attemptFailure) { report.go
59 > if len(failures) == 0 {
60 > return report.go
61 > }
62
63 var b strings.Builder
go.temporal.io/server/common/testing/await/config.go 17 covered LOC · 3 ranges

Open complete file

17 }
18
19 > func newConfig() config { config.go
20 > return config{
21 > attemptTimeout: envDuration(attemptTimeoutEnvVar, 10*time.Second) * debug.TimeoutMultiplier,
22 > }
23 > }
24
25 > func legacyConfig(timeout, pollInterval time.Duration, timeoutMsg string) config { config.go
26 > cfg := newConfig()
27 > cfg.totalTimeout = timeout
28 > cfg.pollInterval = pollInterval
29 > cfg.timeoutMsg = timeoutMsg
30 > return cfg
31 > }
32
33 > func envDuration(name string, fallback time.Duration) time.Duration { config.go
34 > if s := os.Getenv(name); s != "" {
35 > if d, err := time.ParseDuration(s); err == nil && d > 0 {
36 > return d
37 > }
38 }
39 return fallback
go.temporal.io/server/common/testing/await/t.go 4 covered LOC · 1 range

Open complete file

24
25 // Context returns the await-scoped context for the current attempt.
26 > func (t *T) Context() context.Context { t.go
27 > if t.ctx != nil {
28 > return t.ctx
29 > }
30 return t.tb.Context()
31 }