11import * as dgram from "node:dgram" ;
2+
3+ import { logDebug } from "../utils" ;
24import { LambdaDogStatsD } from "./dogstatsd" ;
35
46jest . mock ( "node:dgram" , ( ) => ( {
57 createSocket : jest . fn ( ) ,
68} ) ) ;
9+ jest . mock ( "../utils" , ( ) => ( {
10+ logDebug : jest . fn ( ) ,
11+ } ) ) ;
712
813describe ( "LambdaDogStatsD" , ( ) => {
9- let mockSend : jest . Mock ;
10- let mockUnref : jest . Mock ;
14+ let mockSend : jest . Mock < void , [ Buffer , number , string , ( error ?: Error ) => void ] > ;
15+ let mockUnref : jest . Mock < void , [ ] > ;
16+
17+ function useControlledSocket ( ) : Array < ( error ?: Error ) => void > {
18+ const callbacks : Array < ( error ?: Error ) => void > = [ ] ;
19+ mockSend . mockImplementation ( ( _message , _port , _host , callback ) => {
20+ callbacks . push ( callback ) ;
21+ } ) ;
22+ return callbacks ;
23+ }
24+
25+ /**
26+ * @param {Promise<void> } promise
27+ */
28+ function observeCompletion ( promise : Promise < void > ) : jest . Mock < void , [ ] > {
29+ const completed = jest . fn ( ) ;
30+ void promise . then ( completed ) ;
31+ return completed ;
32+ }
1133
1234 beforeEach ( ( ) => {
1335 // A send() that immediately calls its callback
@@ -23,6 +45,7 @@ describe("LambdaDogStatsD", () => {
2345 } ) ;
2446
2547 afterEach ( ( ) => {
48+ jest . useRealTimers ( ) ;
2649 jest . clearAllMocks ( ) ;
2750 } ) ;
2851
@@ -62,8 +85,10 @@ describe("LambdaDogStatsD", () => {
6285 } ) ;
6386
6487 it ( "flush() resolves immediately when there are no sends" , async ( ) => {
88+ jest . useFakeTimers ( ) ;
6589 const client = new LambdaDogStatsD ( ) ;
6690 await expect ( client . flush ( ) ) . resolves . toBeUndefined ( ) ;
91+ expect ( jest . getTimerCount ( ) ) . toBe ( 0 ) ;
6792 } ) ;
6893
6994 it ( "unrefs socket on client creation" , ( ) => {
@@ -72,26 +97,145 @@ describe("LambdaDogStatsD", () => {
7297 expect ( mockUnref ) . toHaveBeenCalledTimes ( 1 ) ;
7398 } ) ;
7499
75- it ( "flush() times out if a send never invokes its callback" , async ( ) => {
76- // replace socket.send with a never‐calling callback
77- ( dgram . createSocket as jest . Mock ) . mockReturnValue ( {
78- send : jest . fn ( ) , // never calls callback
79- unref : jest . fn ( ) ,
80- getSendBufferSize : jest . fn ( ) ,
81- setSendBufferSize : jest . fn ( ) ,
82- bind : jest . fn ( ) ,
100+ it ( "flush() waits for one pending send and clears its timeout" , async ( ) => {
101+ jest . useFakeTimers ( ) ;
102+ const callbacks = useControlledSocket ( ) ;
103+ const client = new LambdaDogStatsD ( ) ;
104+ client . distribution ( "pending" , 1 ) ;
105+
106+ const flush = client . flush ( ) ;
107+ const completed = observeCompletion ( flush ) ;
108+ await Promise . resolve ( ) ;
109+ expect ( completed ) . not . toHaveBeenCalled ( ) ;
110+
111+ callbacks [ 0 ] ( ) ;
112+ await expect ( flush ) . resolves . toBeUndefined ( ) ;
113+ expect ( jest . getTimerCount ( ) ) . toBe ( 0 ) ;
114+ } ) ;
115+
116+ it ( "flush() waits for every pending send" , async ( ) => {
117+ const callbacks = useControlledSocket ( ) ;
118+ const client = new LambdaDogStatsD ( ) ;
119+ client . distribution ( "first" , 1 ) ;
120+ client . distribution ( "second" , 2 ) ;
121+ client . distribution ( "third" , 3 ) ;
122+
123+ const flush = client . flush ( ) ;
124+ const completed = observeCompletion ( flush ) ;
125+ callbacks [ 0 ] ( ) ;
126+ callbacks [ 1 ] ( ) ;
127+ await Promise . resolve ( ) ;
128+ expect ( completed ) . not . toHaveBeenCalled ( ) ;
129+
130+ callbacks [ 2 ] ( ) ;
131+ await expect ( flush ) . resolves . toBeUndefined ( ) ;
132+ } ) ;
133+
134+ it ( "flush() waits for sends started while it is pending" , async ( ) => {
135+ const callbacks = useControlledSocket ( ) ;
136+ const client = new LambdaDogStatsD ( ) ;
137+ client . distribution ( "first" , 1 ) ;
138+
139+ const flush = client . flush ( ) ;
140+ const completed = observeCompletion ( flush ) ;
141+ client . distribution ( "second" , 2 ) ;
142+ callbacks [ 0 ] ( ) ;
143+ await Promise . resolve ( ) ;
144+ expect ( completed ) . not . toHaveBeenCalled ( ) ;
145+
146+ callbacks [ 1 ] ( ) ;
147+ await expect ( flush ) . resolves . toBeUndefined ( ) ;
148+ } ) ;
149+
150+ it ( "flush() settles concurrent callers after the pending send" , async ( ) => {
151+ const callbacks = useControlledSocket ( ) ;
152+ const client = new LambdaDogStatsD ( ) ;
153+ client . distribution ( "pending" , 1 ) ;
154+
155+ const firstFlush = client . flush ( ) ;
156+ const secondFlush = client . flush ( ) ;
157+ expect ( secondFlush ) . toBe ( firstFlush ) ;
158+ callbacks [ 0 ] ( ) ;
159+
160+ await Promise . all ( [ firstFlush , secondFlush ] ) ;
161+ } ) ;
162+
163+ it ( "logs callback errors and completes the send" , async ( ) => {
164+ jest . useFakeTimers ( ) ;
165+ const error = new Error ( "callback failure" ) ;
166+ const callbacks = useControlledSocket ( ) ;
167+ const client = new LambdaDogStatsD ( ) ;
168+
169+ client . distribution ( "metric" , 1 ) ;
170+
171+ const flush = client . flush ( ) ;
172+ callbacks [ 0 ] ( error ) ;
173+ expect ( jest . getTimerCount ( ) ) . toBe ( 0 ) ;
174+ await expect ( flush ) . resolves . toBeUndefined ( ) ;
175+ expect ( logDebug ) . toHaveBeenCalledWith ( "Unable to send metric packet: callback failure" ) ;
176+ } ) ;
177+
178+ it ( "logs synchronous socket errors without throwing" , async ( ) => {
179+ jest . useFakeTimers ( ) ;
180+ mockSend . mockImplementation ( ( ) => {
181+ throw new Error ( "synchronous failure" ) ;
83182 } ) ;
183+ const client = new LambdaDogStatsD ( ) ;
84184
185+ client . distribution ( "metric" , 1 ) ;
186+
187+ const flush = client . flush ( ) ;
188+ expect ( jest . getTimerCount ( ) ) . toBe ( 0 ) ;
189+ await expect ( flush ) . resolves . toBeUndefined ( ) ;
190+ expect ( logDebug ) . toHaveBeenCalledWith ( "Unable to send metric packet: synchronous failure" ) ;
191+ } ) ;
192+
193+ it ( "normalizes non-Error values thrown synchronously by the socket" , async ( ) => {
194+ jest . useFakeTimers ( ) ;
195+ mockSend . mockImplementation ( ( ) => {
196+ throw "non-error failure" ;
197+ } ) ;
85198 const client = new LambdaDogStatsD ( ) ;
86- client . distribution ( "will" , 9 ) ;
87199
200+ client . distribution ( "metric" , 1 ) ;
201+
202+ const flush = client . flush ( ) ;
203+ expect ( jest . getTimerCount ( ) ) . toBe ( 0 ) ;
204+ await expect ( flush ) . resolves . toBeUndefined ( ) ;
205+ expect ( logDebug ) . toHaveBeenCalledWith ( "Unable to send metric packet: Unknown socket send failure" ) ;
206+ } ) ;
207+
208+ it ( "flush() times out if a send never invokes its callback" , async ( ) => {
88209 jest . useFakeTimers ( ) ;
89- const p = client . flush ( ) ;
90- // advance past the 1000ms MAX_FLUSH_TIMEOUT
91- jest . advanceTimersByTime ( 1100 ) ;
210+ useControlledSocket ( ) ;
92211
93- // expect the Promise returned by flush() to resolve successfully
94- await expect ( p ) . resolves . toBeUndefined ( ) ;
95- jest . useRealTimers ( ) ;
212+ const client = new LambdaDogStatsD ( ) ;
213+ client . distribution ( "will" , 9 ) ;
214+
215+ const flush = client . flush ( ) ;
216+ jest . advanceTimersByTime ( 1000 ) ;
217+
218+ await expect ( flush ) . resolves . toBeUndefined ( ) ;
219+ expect ( logDebug ) . toHaveBeenCalledWith ( "Timed out before sending all metric payloads" ) ;
220+ } ) ;
221+
222+ it ( "ignores late callbacks from a timed-out flush when the client is reused" , async ( ) => {
223+ jest . useFakeTimers ( ) ;
224+ const callbacks = useControlledSocket ( ) ;
225+ const client = new LambdaDogStatsD ( ) ;
226+ client . distribution ( "old" , 1 ) ;
227+ const oldFlush = client . flush ( ) ;
228+ jest . advanceTimersByTime ( 1000 ) ;
229+ await oldFlush ;
230+
231+ client . distribution ( "new" , 2 ) ;
232+ const newFlush = client . flush ( ) ;
233+ const completed = observeCompletion ( newFlush ) ;
234+ callbacks [ 0 ] ( ) ;
235+ await Promise . resolve ( ) ;
236+ expect ( completed ) . not . toHaveBeenCalled ( ) ;
237+
238+ callbacks [ 1 ] ( ) ;
239+ await expect ( newFlush ) . resolves . toBeUndefined ( ) ;
96240 } ) ;
97241} ) ;
0 commit comments