| 882 | } |
| 883 | |
| 884 | func (m *SyncMockManager) ResolveRange(start, end time.Time, testName string, keep bool, mapping bool) { |
| 885 | // Collect mocks and mapping data under the lock, then send to the |
| 886 | // outgoing channels AFTER releasing it. Holding m.mu across a |
| 887 | // channel send can deadlock on ordering: a buffer-full outChan |
| 888 | // would keep mu held, blocking every AddMock waiting to enqueue. |
| 889 | // We have outChanMu (inside sendToOutChan) to guard the actual |
| 890 | // send against close, so m.mu release here is safe. |
| 891 | var mocksToSend []*models.Mock |
| 892 | var associatedMockIDs []string |
| 893 | var mappingEntry *models.TestMockMapping |
| 894 | // lateMappings accumulates mock IDs for mocks retroactively binned |
| 895 | // into a PAST (already-resolved) window, keyed by that window's test |
| 896 | // name. lateBinned counts them for the buffer-transition diagnostic. |
| 897 | var lateMappings map[string][]string |
| 898 | lateBinned := 0 |
| 899 | |
| 900 | m.mu.Lock() |
| 901 | // Snapshot the outChan wiring status under outChanMu (NOT m.mu) |
| 902 | // so we don't race SetOutputChannel / CloseOutChan. Only the |
| 903 | // bound boolean is needed — the actual send later goes through |
| 904 | // sendToOutChan which reacquires the RLock and will skip the |
| 905 | // send itself when outChanClosed is true. |
| 906 | outChanBound, _ := m.outChanStatus() |
| 907 | mappingChan := m.mappingChan |
| 908 | |
| 909 | // A kept resolve (keep==true) is one UNIQUE recorded test case; advance the |
| 910 | // startup-window counter so AddMock stops tagging mocks IsStartup once we are |
| 911 | // past the Nth test. Duplicates resolve with keep==false (static dedup) and |
| 912 | // must NOT count — the window is measured in recorded tests, not requests. |
| 913 | // Incrementing here only gates FUTURE ingests; the mocks processed in this |
| 914 | // call were already tagged (or not) at AddMock time, and the rescues below |
| 915 | // key off that per-mock tag, not the live counter. |
| 916 | if keep { |
| 917 | m.resolvedTestCount++ |
| 918 | } |
| 919 | |
| 920 | // Stale-buffer safety valve. |
| 921 | // |
| 922 | // The check exists to bound buffer growth when a stream of mocks |
| 923 | // arrives that is never closed off by a corresponding test-window |
| 924 | // resolve (e.g. a parser kept emitting after the test ended). |
| 925 | // Without it, m.buffer would grow without bound across a long |
| 926 | // recording session. |
| 927 | // |
| 928 | // CRITICAL ordering: cutoff must NOT pre-empt the window match. |
| 929 | // A long-running test (mongo fuzzer's curl /run takes ~56 s for |
| 930 | // 10 000 ops) emits per-test mocks whose ReqTimestampMock is far |
| 931 | // older than 7 s by the time ResolveRange fires at request |
| 932 | // completion — but those mocks ARE in-window and must be flushed |
| 933 | // to the recorder, not silently dropped. The pre-c53b4906 V2 path |
| 934 | // bypassed syncMock entirely so the cutoff never applied; routing |
| 935 | // through AddMock (#4122) made the previous "cutoff first" ordering |
| 936 | // drop the first ~49 s of a 56 s recording window, leaving replay |
| 937 | // without the mongo handshake mocks → connection-pool error at |
| 938 | // driver init. |
| 939 | // |
| 940 | // New ordering: |
| 941 | // 1. In-window matches are kept/forwarded regardless of age. |