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,135 @@ 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+ const error = new Error ( "callback failure" ) ;
165+ mockSend . mockImplementation ( ( _message , _port , _host , callback ) => callback ( error ) ) ;
166+ const client = new LambdaDogStatsD ( ) ;
167+
168+ client . distribution ( "metric" , 1 ) ;
169+
170+ await expect ( client . flush ( ) ) . resolves . toBeUndefined ( ) ;
171+ expect ( logDebug ) . toHaveBeenCalledWith ( "Unable to send metric packet: callback failure" ) ;
172+ } ) ;
173+
174+ it ( "logs synchronous socket errors without throwing" , async ( ) => {
175+ mockSend . mockImplementation ( ( ) => {
176+ throw new Error ( "synchronous failure" ) ;
83177 } ) ;
178+ const client = new LambdaDogStatsD ( ) ;
84179
180+ client . distribution ( "metric" , 1 ) ;
181+
182+ await expect ( client . flush ( ) ) . resolves . toBeUndefined ( ) ;
183+ expect ( logDebug ) . toHaveBeenCalledWith ( "Unable to send metric packet: synchronous failure" ) ;
184+ } ) ;
185+
186+ it ( "normalizes non-Error values thrown synchronously by the socket" , async ( ) => {
187+ mockSend . mockImplementation ( ( ) => {
188+ throw "non-error failure" ;
189+ } ) ;
85190 const client = new LambdaDogStatsD ( ) ;
86- client . distribution ( "will" , 9 ) ;
87191
192+ client . distribution ( "metric" , 1 ) ;
193+
194+ await expect ( client . flush ( ) ) . resolves . toBeUndefined ( ) ;
195+ expect ( logDebug ) . toHaveBeenCalledWith ( "Unable to send metric packet: Unknown socket send failure" ) ;
196+ } ) ;
197+
198+ it ( "flush() times out if a send never invokes its callback" , async ( ) => {
88199 jest . useFakeTimers ( ) ;
89- const p = client . flush ( ) ;
90- // advance past the 1000ms MAX_FLUSH_TIMEOUT
91- jest . advanceTimersByTime ( 1100 ) ;
200+ useControlledSocket ( ) ;
92201
93- // expect the Promise returned by flush() to resolve successfully
94- await expect ( p ) . resolves . toBeUndefined ( ) ;
95- jest . useRealTimers ( ) ;
202+ const client = new LambdaDogStatsD ( ) ;
203+ client . distribution ( "will" , 9 ) ;
204+
205+ const flush = client . flush ( ) ;
206+ jest . advanceTimersByTime ( 1000 ) ;
207+
208+ await expect ( flush ) . resolves . toBeUndefined ( ) ;
209+ expect ( logDebug ) . toHaveBeenCalledWith ( "Timed out before sending all metric payloads" ) ;
210+ } ) ;
211+
212+ it ( "ignores late callbacks from a timed-out flush when the client is reused" , async ( ) => {
213+ jest . useFakeTimers ( ) ;
214+ const callbacks = useControlledSocket ( ) ;
215+ const client = new LambdaDogStatsD ( ) ;
216+ client . distribution ( "old" , 1 ) ;
217+ const oldFlush = client . flush ( ) ;
218+ jest . advanceTimersByTime ( 1000 ) ;
219+ await oldFlush ;
220+
221+ client . distribution ( "new" , 2 ) ;
222+ const newFlush = client . flush ( ) ;
223+ const completed = observeCompletion ( newFlush ) ;
224+ callbacks [ 0 ] ( ) ;
225+ await Promise . resolve ( ) ;
226+ expect ( completed ) . not . toHaveBeenCalled ( ) ;
227+
228+ callbacks [ 1 ] ( ) ;
229+ await expect ( newFlush ) . resolves . toBeUndefined ( ) ;
96230 } ) ;
97231} ) ;
0 commit comments