master
go 762 lines 20.8 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package functions
4
5 import (
6 "bytes"
7 "context"
8 "fmt"
9 "strings"
10 "sync"
11 "sync/atomic"
12 "testing"
13 "time"
14
15 "github.com/netdata/netdata/go/plugins/pkg/netdataapi"
16 "github.com/stretchr/testify/assert"
17 "github.com/stretchr/testify/require"
18 )
19
20 const (
21 testPermissions = "0xFFFF"
22 testSource = "method=api,role=test"
23 )
24
25 type chanInput struct {
26 ch chan string
27 }
28
29 func (m *chanInput) lines() <-chan string {
30 return m.ch
31 }
32
33 type safeBuffer struct {
34 mu sync.Mutex
35 b bytes.Buffer
36 }
37
38 func (s *safeBuffer) Write(p []byte) (int, error) {
39 s.mu.Lock()
40 defer s.mu.Unlock()
41 return s.b.Write(p)
42 }
43
44 func (s *safeBuffer) String() string {
45 s.mu.Lock()
46 defer s.mu.Unlock()
47 return s.b.String()
48 }
49
50 func newFlowManager() (*Manager, *safeBuffer) {
51 mgr := NewManager()
52 buf := &safeBuffer{}
53 mgr.api = netdataapi.New(buf)
54 return mgr, buf
55 }
56
57 func functionLine(uid, name string) string {
58 return fmt.Sprintf(`FUNCTION %s 10 "%s" %s "%s"`, uid, name, testPermissions, testSource)
59 }
60
61 func payloadStartCmd(uid, name string) string {
62 return fmt.Sprintf(`FUNCTION_PAYLOAD %s 10 "%s" %s "%s" application/json`, uid, name, testPermissions, testSource)
63 }
64
65 func waitForSubstring(t *testing.T, f func() string, substr string, timeout time.Duration) {
66 t.Helper()
67
68 deadline := time.Now().Add(timeout)
69 for time.Now().Before(deadline) {
70 if strings.Contains(f(), substr) {
71 return
72 }
73 time.Sleep(10 * time.Millisecond)
74 }
75 t.Fatalf("timeout waiting for substring %q in output: %s", substr, f())
76 }
77
78 func startFlowManager(t *testing.T, mgr *Manager) (context.CancelFunc, chan struct{}) {
79 t.Helper()
80
81 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
82 done := make(chan struct{})
83 go func() {
84 defer close(done)
85 mgr.Run(ctx, nil)
86 }()
87 return cancel, done
88 }
89
90 func waitForDone(t *testing.T, done <-chan struct{}) {
91 t.Helper()
92
93 select {
94 case <-done:
95 case <-time.After(3 * time.Second):
96 t.Fatal("timeout waiting for manager run to complete")
97 }
98 }
99
100 func waitForCondition(t *testing.T, timeout time.Duration, fn func() bool, desc string) {
101 t.Helper()
102
103 deadline := time.Now().Add(timeout)
104 for time.Now().Before(deadline) {
105 if fn() {
106 return
107 }
108 time.Sleep(10 * time.Millisecond)
109 }
110 t.Fatalf("timeout waiting for condition: %s", desc)
111 }
112
113 func TestManager_FlowScenarios(t *testing.T) {
114 tests := map[string]struct {
115 run func(t *testing.T, mgr *Manager, in *chanInput, out *safeBuffer)
116 }{
117 "queued cancel is ignored; function still executes": {
118 run: func(t *testing.T, mgr *Manager, in *chanInput, out *safeBuffer) {
119 // New semantics: a CANCEL for a queued function is a no-op at
120 // the framework level. The function stays in the scheduler,
121 // runs to completion when its turn comes, and any terminal
122 // response goes through normally. No 499 is emitted by the
123 // framework (netdata already considers the transaction done
124 // via its own per-transaction timer, so an extra 499 would
125 // just be noise).
126 var (
127 mu sync.Mutex
128 executed []string
129 )
130 started := make(chan struct{}, 1)
131 release := make(chan struct{})
132 tx2Done := make(chan struct{})
133
134 mgr.Register("fn", func(fn Function) {
135 mu.Lock()
136 executed = append(executed, fn.UID)
137 mu.Unlock()
138 if fn.UID == "tx1" {
139 started <- struct{}{}
140 <-release
141 // Emit terminal response so the per-key lane advances
142 // to tx2; otherwise the lane stays "owned" by tx1 and
143 // tx2 never gets picked up.
144 mgr.respUID(fn.UID, 200, "ok")
145 }
146 if fn.UID == "tx2" {
147 mgr.respUID(fn.UID, 200, "ok")
148 close(tx2Done)
149 }
150 })
151
152 cancel, done := startFlowManager(t, mgr)
153 defer cancel()
154
155 in.ch <- functionLine("tx1", "fn")
156 <-started
157 in.ch <- functionLine("tx2", "fn")
158 in.ch <- "FUNCTION_CANCEL tx2"
159
160 // tx2 must NOT receive a 499 from the framework. We can't
161 // assert "no 499 ever" without waiting forever, but we can at
162 // least verify it isn't there immediately after CANCEL.
163 time.Sleep(50 * time.Millisecond)
164 assert.NotContains(t, out.String(), "FUNCTION_RESULT_BEGIN tx2 499")
165
166 close(release)
167 select {
168 case <-tx2Done:
169 case <-time.After(time.Second):
170 t.Fatal("tx2 did not execute after release")
171 }
172
173 close(in.ch)
174 waitForDone(t, done)
175
176 // Both tx1 and tx2 should have executed (in order).
177 mu.Lock()
178 defer mu.Unlock()
179 assert.Equal(t, []string{"tx1", "tx2"}, executed)
180 // tx2's normal 200 response went through (no tombstone).
181 assert.Contains(t, out.String(), "FUNCTION_RESULT_BEGIN tx2 200")
182 },
183 },
184 "running cancel fallback tombstones silently (no emit)": {
185 run: func(t *testing.T, mgr *Manager, in *chanInput, out *safeBuffer) {
186 // New semantics: the cancel fallback timer no longer emits a
187 // 499 to netdata. By the time CANCEL arrives, netdata has
188 // already 504'd and removed the inflight transaction, so any
189 // response would just produce a "transaction not found" log.
190 // We only do the bookkeeping (tombstone + lane advance).
191 mgr.cancelFallbackDelay = 50 * time.Millisecond
192 started := make(chan struct{}, 1)
193 release := make(chan struct{})
194
195 mgr.Register("fn", func(fn Function) {
196 if fn.UID == "tx1" {
197 started <- struct{}{}
198 <-release
199 }
200 })
201
202 cancel, done := startFlowManager(t, mgr)
203 defer cancel()
204
205 in.ch <- functionLine("tx1", "fn")
206 <-started
207 in.ch <- "FUNCTION_CANCEL tx1"
208 // Wait long enough for the fallback timer to fire.
209 time.Sleep(150 * time.Millisecond)
210
211 close(release)
212 close(in.ch)
213 waitForDone(t, done)
214
215 // No 499 should be emitted, ever.
216 assert.Equal(t, 0, strings.Count(out.String(), "FUNCTION_RESULT_BEGIN tx1 499"))
217 },
218 },
219 "repeated cancel for same uid is idempotent and silent": {
220 run: func(t *testing.T, mgr *Manager, in *chanInput, out *safeBuffer) {
221 mgr.cancelFallbackDelay = 50 * time.Millisecond
222 started := make(chan struct{}, 1)
223 release := make(chan struct{})
224
225 mgr.Register("fn", func(fn Function) {
226 if fn.UID == "tx1" {
227 started <- struct{}{}
228 <-release
229 }
230 })
231
232 cancel, done := startFlowManager(t, mgr)
233 defer cancel()
234
235 in.ch <- functionLine("tx1", "fn")
236 <-started
237 in.ch <- "FUNCTION_CANCEL tx1"
238 in.ch <- "FUNCTION_CANCEL tx1"
239 time.Sleep(150 * time.Millisecond)
240
241 close(release)
242 close(in.ch)
243 waitForDone(t, done)
244
245 assert.Equal(t, 0, strings.Count(out.String(), "FUNCTION_RESULT_BEGIN tx1 499"))
246 },
247 },
248 "running cancel drops late terminal response": {
249 run: func(t *testing.T, mgr *Manager, in *chanInput, out *safeBuffer) {
250 mgr.cancelFallbackDelay = 50 * time.Millisecond
251 started := make(chan struct{}, 1)
252 release := make(chan struct{})
253
254 mgr.Register("fn", func(fn Function) {
255 if fn.UID == "tx1" {
256 started <- struct{}{}
257 <-release
258 mgr.respUID(fn.UID, 200, "late response")
259 }
260 })
261
262 cancel, done := startFlowManager(t, mgr)
263 defer cancel()
264
265 in.ch <- functionLine("tx1", "fn")
266 <-started
267 in.ch <- "FUNCTION_CANCEL tx1"
268 // Let the fallback timer fire (tombstones the UID).
269 time.Sleep(150 * time.Millisecond)
270
271 close(release)
272 close(in.ch)
273 waitForDone(t, done)
274
275 got := out.String()
276 // No 499 emitted by the fallback path.
277 assert.Equal(t, 0, strings.Count(got, "FUNCTION_RESULT_BEGIN tx1 499"))
278 // Late 200 from the handler is dropped by the tombstone.
279 assert.Equal(t, 0, strings.Count(got, "FUNCTION_RESULT_BEGIN tx1 200"))
280 },
281 },
282 "payload pre-admission cancel emits 499 and skips handler": {
283 run: func(t *testing.T, mgr *Manager, in *chanInput, out *safeBuffer) {
284 var calls atomic.Int32
285 mgr.Register("fn", func(Function) { calls.Add(1) })
286
287 cancel, done := startFlowManager(t, mgr)
288 defer cancel()
289
290 in.ch <- payloadStartCmd("tx1", "fn")
291 in.ch <- "payload line"
292 in.ch <- "FUNCTION_CANCEL tx1"
293 close(in.ch)
294 waitForDone(t, done)
295
296 waitForSubstring(t, out.String, "FUNCTION_RESULT_BEGIN tx1 499", time.Second)
297 assert.EqualValues(t, 0, calls.Load())
298 },
299 },
300 "duplicate active uid is ignored without corrupting lane progression": {
301 run: func(t *testing.T, mgr *Manager, in *chanInput, out *safeBuffer) {
302 mgr.workerCount = 2
303 started := make(chan struct{}, 1)
304 tx2Started := make(chan struct{}, 1)
305 release := make(chan struct{})
306 var calls atomic.Int32
307
308 mgr.Register("fn", func(fn Function) {
309 calls.Add(1)
310
311 if fn.UID == "tx1" {
312 started <- struct{}{}
313 <-release
314 mgr.respUID(fn.UID, 200, "ok")
315 return
316 }
317
318 if fn.UID == "tx2" {
319 select {
320 case tx2Started <- struct{}{}:
321 default:
322 }
323 }
324
325 mgr.respUID(fn.UID, 200, "ok")
326 })
327
328 cancel, done := startFlowManager(t, mgr)
329 defer cancel()
330
331 in.ch <- functionLine("tx1", "fn")
332 <-started
333 // Same-key request should stay queued until tx1 completes.
334 in.ch <- functionLine("tx2", "fn")
335 // Duplicate active UID must not advance lanes or finalize tx1.
336 in.ch <- functionLine("tx1", "fn")
337
338 select {
339 case <-tx2Started:
340 t.Fatal("tx2 started before tx1 was released")
341 default:
342 }
343
344 close(release)
345 select {
346 case <-tx2Started:
347 case <-time.After(time.Second):
348 t.Fatal("tx2 did not start after tx1 was released")
349 }
350 close(in.ch)
351 waitForDone(t, done)
352
353 got := out.String()
354 assert.Equal(t, 1, strings.Count(got, "FUNCTION_RESULT_BEGIN tx1 200"))
355 assert.Equal(t, 1, strings.Count(got, "FUNCTION_RESULT_BEGIN tx2 200"))
356 assert.Equal(t, 0, strings.Count(got, "FUNCTION_RESULT_BEGIN tx1 409"))
357 assert.EqualValues(t, 2, calls.Load())
358 },
359 },
360 "duplicate tombstoned uid is ignored without extra terminal output": {
361 run: func(t *testing.T, mgr *Manager, in *chanInput, out *safeBuffer) {
362 var calls atomic.Int32
363
364 mgr.Register("fn", func(fn Function) {
365 calls.Add(1)
366 mgr.respUID(fn.UID, 200, "ok")
367 })
368
369 cancel, done := startFlowManager(t, mgr)
370 defer cancel()
371
372 in.ch <- functionLine("tx1", "fn")
373 waitForSubstring(t, out.String, "FUNCTION_RESULT_BEGIN tx1 200", time.Second)
374
375 // Re-send same UID while tombstone is still active.
376 in.ch <- functionLine("tx1", "fn")
377 close(in.ch)
378 waitForDone(t, done)
379
380 got := out.String()
381 assert.Equal(t, 1, strings.Count(got, "FUNCTION_RESULT_BEGIN tx1 200"))
382 assert.Equal(t, 0, strings.Count(got, "FUNCTION_RESULT_BEGIN tx1 409"))
383 assert.EqualValues(t, 1, calls.Load())
384 },
385 },
386 "panic in handler emits 500": {
387 run: func(t *testing.T, mgr *Manager, in *chanInput, out *safeBuffer) {
388 mgr.Register("fn", func(Function) { panic("boom") })
389
390 cancel, done := startFlowManager(t, mgr)
391 defer cancel()
392
393 in.ch <- functionLine("tx1", "fn")
394 close(in.ch)
395 waitForDone(t, done)
396
397 waitForSubstring(t, out.String, "FUNCTION_RESULT_BEGIN tx1 500", time.Second)
398 },
399 },
400 "cancel for unknown uid is no-op": {
401 run: func(t *testing.T, mgr *Manager, in *chanInput, out *safeBuffer) {
402 cancel, done := startFlowManager(t, mgr)
403 defer cancel()
404
405 in.ch <- "FUNCTION_CANCEL unknown"
406 close(in.ch)
407 waitForDone(t, done)
408
409 assert.Equal(t, 0, strings.Count(out.String(), "FUNCTION_RESULT_BEGIN"))
410 },
411 },
412 "cancel after completion is no-op": {
413 run: func(t *testing.T, mgr *Manager, in *chanInput, out *safeBuffer) {
414 mgr.Register("fn", func(fn Function) {
415 mgr.respUID(fn.UID, 200, "done")
416 })
417
418 cancel, done := startFlowManager(t, mgr)
419 defer cancel()
420
421 in.ch <- functionLine("tx1", "fn")
422 waitForSubstring(t, out.String, "FUNCTION_RESULT_BEGIN tx1 200", time.Second)
423 in.ch <- "FUNCTION_CANCEL tx1"
424 close(in.ch)
425 waitForDone(t, done)
426
427 got := out.String()
428 assert.Equal(t, 1, strings.Count(got, "FUNCTION_RESULT_BEGIN tx1 200"))
429 assert.Equal(t, 0, strings.Count(got, "FUNCTION_RESULT_BEGIN tx1 499"))
430 },
431 },
432 "stdin close uses canceling shutdown and force-finalizes unresolved requests": {
433 run: func(t *testing.T, mgr *Manager, in *chanInput, out *safeBuffer) {
434 mgr.shutdownDrainTimeout = 50 * time.Millisecond
435 started := make(chan struct{}, 1)
436 block := make(chan struct{})
437
438 mgr.Register("fn", func(fn Function) {
439 started <- struct{}{}
440 <-block
441 mgr.respUID(fn.UID, 200, "late")
442 })
443
444 cancel, done := startFlowManager(t, mgr)
445 defer cancel()
446
447 in.ch <- functionLine("tx1", "fn")
448 <-started
449 close(in.ch)
450 waitForDone(t, done)
451
452 got := out.String()
453 assert.Equal(t, 1, strings.Count(got, "FUNCTION_RESULT_BEGIN tx1 499"))
454 assert.Equal(t, 0, strings.Count(got, "FUNCTION_RESULT_BEGIN tx1 200"))
455 },
456 },
457 "late terminal output after shutdown is dropped by tombstone guard": {
458 run: func(t *testing.T, mgr *Manager, in *chanInput, out *safeBuffer) {
459 mgr.shutdownDrainTimeout = 50 * time.Millisecond
460 started := make(chan struct{}, 1)
461 block := make(chan struct{})
462 doneResp := make(chan struct{})
463
464 mgr.Register("fn", func(fn Function) {
465 started <- struct{}{}
466 <-block
467 mgr.respUID(fn.UID, 200, "late")
468 close(doneResp)
469 })
470
471 cancel, done := startFlowManager(t, mgr)
472 defer cancel()
473
474 in.ch <- functionLine("tx1", "fn")
475 <-started
476 close(in.ch)
477 waitForDone(t, done)
478
479 // Release handler after manager has already force-finalized and returned.
480 close(block)
481 select {
482 case <-doneResp:
483 case <-time.After(time.Second):
484 t.Fatal("timed out waiting for late handler response")
485 }
486
487 got := out.String()
488 assert.Equal(t, 1, strings.Count(got, "FUNCTION_RESULT_BEGIN tx1 499"))
489 assert.Equal(t, 0, strings.Count(got, "FUNCTION_RESULT_BEGIN tx1 200"))
490 },
491 },
492 "shutdown finalizes unresolved awaiting_result even when workers are drained": {
493 run: func(t *testing.T, mgr *Manager, in *chanInput, out *safeBuffer) {
494 mgr.shutdownDrainTimeout = 250 * time.Millisecond
495 mgr.awaitingWarnDelay = time.Second
496 returned := make(chan struct{}, 1)
497
498 mgr.Register("fn", func(Function) {
499 // Return without terminal response.
500 returned <- struct{}{}
501 })
502
503 cancel, done := startFlowManager(t, mgr)
504 defer cancel()
505
506 in.ch <- functionLine("tx1", "fn")
507 <-returned
508 waitForCondition(t, time.Second, func() bool {
509 mgr.invStateMux.Lock()
510 defer mgr.invStateMux.Unlock()
511 rec, ok := mgr.invState["tx1"]
512 return ok && rec != nil && rec.state == stateAwaitingResult
513 }, "tx1 reaches awaiting_result before shutdown")
514
515 close(in.ch)
516 waitForDone(t, done)
517
518 got := out.String()
519 assert.Equal(t, 1, strings.Count(got, "FUNCTION_RESULT_BEGIN tx1 499"))
520 assert.Equal(t, 0, strings.Count(got, "FUNCTION_RESULT_BEGIN tx1 200"))
521 },
522 },
523 }
524
525 for name, tc := range tests {
526 t.Run(name, func(t *testing.T) {
527 mgr, out := newFlowManager()
528 in := &chanInput{ch: make(chan string, 16)}
529 mgr.input = in
530 tc.run(t, mgr, in, out)
531 })
532 }
533 }
534
535 func TestManager_InvocationStateScenarios(t *testing.T) {
536 tests := map[string]struct {
537 run func(t *testing.T, mgr *Manager, in *chanInput, out *safeBuffer)
538 }{
539 "worker return transitions to awaiting_result until terminal output": {
540 run: func(t *testing.T, mgr *Manager, in *chanInput, out *safeBuffer) {
541 mgr.workerCount = 1
542 returned := make(chan struct{}, 1)
543 releaseResponse := make(chan struct{})
544
545 mgr.Register("fn", func(fn Function) {
546 go func() {
547 <-releaseResponse
548 mgr.respUID(fn.UID, 200, "ok")
549 }()
550 returned <- struct{}{}
551 })
552
553 cancel, done := startFlowManager(t, mgr)
554 defer cancel()
555
556 in.ch <- functionLine("tx1", "fn")
557 <-returned
558
559 waitForCondition(t, time.Second, func() bool {
560 mgr.invStateMux.Lock()
561 defer mgr.invStateMux.Unlock()
562 rec, ok := mgr.invState["tx1"]
563 return ok && rec != nil && rec.state == stateAwaitingResult
564 }, "transaction tx1 reaches awaiting_result state")
565
566 close(releaseResponse)
567 waitForSubstring(t, out.String, "FUNCTION_RESULT_BEGIN tx1 200", time.Second)
568
569 waitForCondition(t, time.Second, func() bool {
570 mgr.invStateMux.Lock()
571 defer mgr.invStateMux.Unlock()
572 _, ok := mgr.invState["tx1"]
573 return !ok
574 }, "transaction tx1 is removed from active state after finalization")
575
576 close(in.ch)
577 waitForDone(t, done)
578 },
579 },
580 }
581
582 for name, tc := range tests {
583 t.Run(name, func(t *testing.T) {
584 mgr, out := newFlowManager()
585 in := &chanInput{ch: make(chan string, 16)}
586 mgr.input = in
587 tc.run(t, mgr, in, out)
588 })
589 }
590 }
591
592 func TestManager_tryFinalize(t *testing.T) {
593 tests := map[string]struct {
594 run func(t *testing.T, mgr *Manager)
595 }{
596 "first finalization wins and tombstone blocks immediate reuse": {
597 run: func(t *testing.T, mgr *Manager) {
598 calls := 0
599 ok := mgr.tryFinalize("uid1", "test.first", func() { calls++ })
600 require.True(t, ok)
601 assert.Equal(t, 1, calls)
602
603 ok = mgr.tryFinalize("uid1", "test.late", func() { calls++ })
604 require.False(t, ok)
605 assert.Equal(t, 1, calls)
606
607 admitted := mgr.trySetInvocationState("uid1", stateQueued, func() {}, "fn")
608 assert.Equal(t, invocationAdmissionDuplicateTombstone, admitted)
609
610 mgr.invStateMux.Lock()
611 mgr.tombstones["uid1"] = time.Now().Add(-time.Second)
612 mgr.invStateMux.Unlock()
613
614 admitted = mgr.trySetInvocationState("uid1", stateQueued, func() {}, "fn")
615 assert.Equal(t, invocationAdmissionAccepted, admitted)
616 },
617 },
618 "empty uid or nil emitter is rejected": {
619 run: func(t *testing.T, mgr *Manager) {
620 assert.False(t, mgr.tryFinalize("", "source", func() {}))
621 assert.False(t, mgr.tryFinalize("uid1", "source", nil))
622 },
623 },
624 }
625
626 for name, tc := range tests {
627 t.Run(name, func(t *testing.T) {
628 tc.run(t, NewManager())
629 })
630 }
631 }
632
633 func TestManager_WorkerPoolConcurrencyBound(t *testing.T) {
634 tests := map[string]struct {
635 workerCount int
636 input []string
637 register func(t *testing.T, mgr *Manager, current, maxSeen *atomic.Int32)
638 assertions func(t *testing.T, maxSeen int32)
639 }{
640 "same key is serialized": {
641 workerCount: 4,
642 input: []string{
643 functionLine("tx-1", "fn"),
644 functionLine("tx-2", "fn"),
645 functionLine("tx-3", "fn"),
646 functionLine("tx-4", "fn"),
647 },
648 register: func(t *testing.T, mgr *Manager, current, maxSeen *atomic.Int32) {
649 t.Helper()
650 mgr.Register("fn", func(fn Function) {
651 c := current.Add(1)
652 for {
653 prev := maxSeen.Load()
654 if c <= prev || maxSeen.CompareAndSwap(prev, c) {
655 break
656 }
657 }
658
659 time.Sleep(30 * time.Millisecond)
660 current.Add(-1)
661 mgr.respUID(fn.UID, 200, "ok")
662 })
663 },
664 assertions: func(t *testing.T, maxSeen int32) {
665 t.Helper()
666 assert.Equal(t, int32(1), maxSeen)
667 },
668 },
669 "different keys run concurrently": {
670 workerCount: 4,
671 input: []string{
672 functionLine("tx-a1", "fnA"),
673 functionLine("tx-b1", "fnB"),
674 functionLine("tx-a2", "fnA"),
675 functionLine("tx-b2", "fnB"),
676 },
677 register: func(t *testing.T, mgr *Manager, current, maxSeen *atomic.Int32) {
678 t.Helper()
679 registerFn := func(name string) {
680 mgr.Register(name, func(fn Function) {
681 c := current.Add(1)
682 for {
683 prev := maxSeen.Load()
684 if c <= prev || maxSeen.CompareAndSwap(prev, c) {
685 break
686 }
687 }
688
689 time.Sleep(40 * time.Millisecond)
690 current.Add(-1)
691 mgr.respUID(fn.UID, 200, "ok")
692 })
693 }
694 registerFn("fnA")
695 registerFn("fnB")
696 },
697 assertions: func(t *testing.T, maxSeen int32) {
698 t.Helper()
699 assert.GreaterOrEqual(t, maxSeen, int32(2))
700 assert.LessOrEqual(t, maxSeen, int32(4))
701 },
702 },
703 }
704
705 for name, tc := range tests {
706 t.Run(name, func(t *testing.T) {
707 mgr, out := newFlowManager()
708 mgr.workerCount = tc.workerCount
709 mgr.queueSize = len(tc.input) + tc.workerCount
710 in := &chanInput{ch: make(chan string, len(tc.input)+tc.workerCount)}
711 mgr.input = in
712
713 var current atomic.Int32
714 var maxSeen atomic.Int32
715
716 tc.register(t, mgr, &current, &maxSeen)
717
718 cancel, done := startFlowManager(t, mgr)
719 defer cancel()
720
721 for _, line := range tc.input {
722 in.ch <- line
723 }
724 close(in.ch)
725 waitForDone(t, done)
726
727 got := out.String()
728 assert.Equal(t, len(tc.input), strings.Count(got, "FUNCTION_RESULT_BEGIN tx-"))
729 tc.assertions(t, maxSeen.Load())
730 })
731 }
732 }
733
734 func TestManager_DispatchInvocationStopping(t *testing.T) {
735 tests := map[string]struct {
736 run func(t *testing.T, mgr *Manager, out *safeBuffer)
737 }{
738 "stopping manager rejects dispatch without tracking state": {
739 run: func(t *testing.T, mgr *Manager, out *safeBuffer) {
740 mgr.Register("fn", func(Function) { t.Fatal("handler should not execute when manager is stopping") })
741 mgr.setStopping(true)
742
743 fn := &Function{UID: "tx-stop", Name: "fn"}
744 mgr.dispatchInvocation(context.Background(), fn)
745
746 waitForSubstring(t, out.String, "FUNCTION_RESULT_BEGIN tx-stop 503", time.Second)
747
748 mgr.invStateMux.Lock()
749 _, ok := mgr.invState["tx-stop"]
750 mgr.invStateMux.Unlock()
751 assert.False(t, ok)
752 },
753 },
754 }
755
756 for name, tc := range tests {
757 t.Run(name, func(t *testing.T) {
758 mgr, out := newFlowManager()
759 tc.run(t, mgr, out)
760 })
761 }
762 }