@@ -9,7 +9,7 @@ use ddcommon::tag::Tag;
9
9
use ddcommon:: Endpoint ;
10
10
use serde:: { Deserialize , Serialize } ;
11
11
use std:: fmt:: Debug ;
12
- use tracing:: { debug , error, info } ;
12
+ use tracing:: error;
13
13
14
14
use anyhow:: anyhow;
15
15
use cadence:: prelude:: * ;
@@ -21,6 +21,7 @@ use ddcommon::connector::uds::socket_path_from_uri;
21
21
use std:: net:: { ToSocketAddrs , UdpSocket } ;
22
22
#[ cfg( unix) ]
23
23
use std:: os:: unix:: net:: UnixDatagram ;
24
+ use std:: sync:: { Arc , Mutex } ;
24
25
25
26
// Queue with a maximum capacity of 32K elements
26
27
const QUEUE_SIZE : usize = 32 * 1024 ;
@@ -67,62 +68,57 @@ pub enum DogStatsDAction<'a, T: AsRef<str>, V: IntoIterator<Item = &'a Tag>> {
67
68
Set ( T , i64 , V ) ,
68
69
}
69
70
70
- /// A dogstatsd-client that flushes stats to a given endpoint. Use `new_flusher` to build one.
71
- #[ derive( Debug ) ]
71
+ /// A dogstatsd-client that flushes stats to a given endpoint.
72
+ #[ derive( Debug , Default ) ]
72
73
pub struct Client {
73
- client : StatsdClient ,
74
+ client : Arc < Mutex < Option < StatsdClient > > > ,
75
+ endpoint : Option < Endpoint > ,
74
76
}
75
77
76
78
/// Build a new flusher instance pointed at the provided endpoint.
77
79
/// Returns error if the provided endpoint is not valid.
78
- pub fn new_flusher ( endpoint : Endpoint ) -> anyhow:: Result < Client > {
80
+ pub fn new ( endpoint : Endpoint ) -> anyhow:: Result < Client > {
81
+ // defer initialization of the client until the first metric is sent and we definitely know the
82
+ // client is going to be used to communicate with the endpoint.
79
83
Ok ( Client {
80
- client : create_client ( & endpoint) ?,
84
+ endpoint : Some ( endpoint) ,
85
+ ..Default :: default ( )
81
86
} )
82
87
}
83
88
84
89
impl Client {
85
- /// Set the destination for dogstatsd metrics, if an API Key is provided the client is disabled
86
- /// as dogstatsd is not allowed in agentless mode. Returns an error if the provided endpoint
87
- /// is invalid.
88
- pub fn set_endpoint ( & mut self , endpoint : Endpoint ) -> anyhow:: Result < ( ) > {
89
- self . client = match endpoint. api_key {
90
- Some ( _) => {
91
- info ! ( "DogStatsD is not available in agentless mode" ) ;
92
- anyhow:: bail!( "DogStatsD is not available in agentless mode" ) ;
93
- }
94
- None => {
95
- debug ! ( "Updating DogStatsD endpoint to {}" , endpoint. url) ;
96
- create_client ( & endpoint) ?
97
- }
98
- } ;
99
- Ok ( ( ) )
100
- }
101
-
102
90
/// Send a vector of DogStatsDActionOwned, this is the same as `send` except it uses the "owned"
103
91
/// version of DogStatsDAction. See the docs for DogStatsDActionOwned for details.
104
92
pub fn send_owned ( & self , actions : Vec < DogStatsDActionOwned > ) {
105
- let client = & self . client ;
93
+ let client_guard = match self . get_or_init_client ( ) {
94
+ Ok ( guard) => guard,
95
+ Err ( e) => {
96
+ error ! ( "Failed to get client: {}" , e) ;
97
+ return ;
98
+ }
99
+ } ;
106
100
107
- for action in actions {
108
- if let Err ( err) = match action {
109
- DogStatsDActionOwned :: Count ( metric, value, tags) => {
110
- do_send ( client. count_with_tags ( metric. as_ref ( ) , value) , & tags)
111
- }
112
- DogStatsDActionOwned :: Distribution ( metric, value, tags) => {
113
- do_send ( client. distribution_with_tags ( metric. as_ref ( ) , value) , & tags)
101
+ if let Some ( client) = & * client_guard {
102
+ for action in actions {
103
+ if let Err ( err) = match action {
104
+ DogStatsDActionOwned :: Count ( metric, value, tags) => {
105
+ do_send ( client. count_with_tags ( metric. as_ref ( ) , value) , & tags)
106
+ }
107
+ DogStatsDActionOwned :: Distribution ( metric, value, tags) => {
108
+ do_send ( client. distribution_with_tags ( metric. as_ref ( ) , value) , & tags)
109
+ }
110
+ DogStatsDActionOwned :: Gauge ( metric, value, tags) => {
111
+ do_send ( client. gauge_with_tags ( metric. as_ref ( ) , value) , & tags)
112
+ }
113
+ DogStatsDActionOwned :: Histogram ( metric, value, tags) => {
114
+ do_send ( client. histogram_with_tags ( metric. as_ref ( ) , value) , & tags)
115
+ }
116
+ DogStatsDActionOwned :: Set ( metric, value, tags) => {
117
+ do_send ( client. set_with_tags ( metric. as_ref ( ) , value) , & tags)
118
+ }
119
+ } {
120
+ error ! ( "Error while sending metric: {}" , err) ;
114
121
}
115
- DogStatsDActionOwned :: Gauge ( metric, value, tags) => {
116
- do_send ( client. gauge_with_tags ( metric. as_ref ( ) , value) , & tags)
117
- }
118
- DogStatsDActionOwned :: Histogram ( metric, value, tags) => {
119
- do_send ( client. histogram_with_tags ( metric. as_ref ( ) , value) , & tags)
120
- }
121
- DogStatsDActionOwned :: Set ( metric, value, tags) => {
122
- do_send ( client. set_with_tags ( metric. as_ref ( ) , value) , & tags)
123
- }
124
- } {
125
- error ! ( "Error while sending metric: {}" , err) ;
126
122
}
127
123
}
128
124
}
@@ -133,31 +129,53 @@ impl Client {
133
129
& self ,
134
130
actions : Vec < DogStatsDAction < ' a , T , V > > ,
135
131
) {
136
- let client = & self . client ;
137
-
138
- for action in actions {
139
- if let Err ( err) = match action {
140
- DogStatsDAction :: Count ( metric, value, tags) => {
141
- let metric_builder = client. count_with_tags ( metric. as_ref ( ) , value) ;
142
- do_send ( metric_builder, tags)
143
- }
144
- DogStatsDAction :: Distribution ( metric, value, tags) => {
145
- do_send ( client. distribution_with_tags ( metric. as_ref ( ) , value) , tags)
146
- }
147
- DogStatsDAction :: Gauge ( metric, value, tags) => {
148
- do_send ( client. gauge_with_tags ( metric. as_ref ( ) , value) , tags)
149
- }
150
- DogStatsDAction :: Histogram ( metric, value, tags) => {
151
- do_send ( client. histogram_with_tags ( metric. as_ref ( ) , value) , tags)
152
- }
153
- DogStatsDAction :: Set ( metric, value, tags) => {
154
- do_send ( client. set_with_tags ( metric. as_ref ( ) , value) , tags)
132
+ let client_guard = match self . get_or_init_client ( ) {
133
+ Ok ( guard) => guard,
134
+ Err ( e) => {
135
+ error ! ( "Failed to get client: {}" , e) ;
136
+ return ;
137
+ }
138
+ } ;
139
+ if let Some ( client) = & * client_guard {
140
+ for action in actions {
141
+ if let Err ( err) = match action {
142
+ DogStatsDAction :: Count ( metric, value, tags) => {
143
+ let metric_builder = client. count_with_tags ( metric. as_ref ( ) , value) ;
144
+ do_send ( metric_builder, tags)
145
+ }
146
+ DogStatsDAction :: Distribution ( metric, value, tags) => {
147
+ do_send ( client. distribution_with_tags ( metric. as_ref ( ) , value) , tags)
148
+ }
149
+ DogStatsDAction :: Gauge ( metric, value, tags) => {
150
+ do_send ( client. gauge_with_tags ( metric. as_ref ( ) , value) , tags)
151
+ }
152
+ DogStatsDAction :: Histogram ( metric, value, tags) => {
153
+ do_send ( client. histogram_with_tags ( metric. as_ref ( ) , value) , tags)
154
+ }
155
+ DogStatsDAction :: Set ( metric, value, tags) => {
156
+ do_send ( client. set_with_tags ( metric. as_ref ( ) , value) , tags)
157
+ }
158
+ } {
159
+ error ! ( "Error while sending metric: {}" , err) ;
155
160
}
156
- } {
157
- error ! ( "Error while sending metric: {}" , err) ;
158
161
}
159
162
}
160
163
}
164
+
165
+ fn get_or_init_client ( & self ) -> anyhow:: Result < std:: sync:: MutexGuard < Option < StatsdClient > > > {
166
+ let mut client_guard = self
167
+ . client
168
+ . lock ( )
169
+ . map_err ( |e| anyhow ! ( "Failed to acquire dogstatsd client lock: {}" , e) ) ?;
170
+
171
+ if client_guard. is_none ( ) {
172
+ if let Some ( endpoint) = & self . endpoint {
173
+ * client_guard = Some ( create_client ( endpoint) ?) ;
174
+ }
175
+ }
176
+
177
+ Ok ( client_guard)
178
+ }
161
179
}
162
180
163
181
fn do_send < ' m , ' t , T , V : IntoIterator < Item = & ' t Tag > > (
@@ -229,13 +247,14 @@ fn create_client(endpoint: &Endpoint) -> anyhow::Result<StatsdClient> {
229
247
#[ cfg( test) ]
230
248
mod test {
231
249
use crate :: DogStatsDAction :: { Count , Distribution , Gauge , Histogram , Set } ;
232
- use crate :: { create_client, new_flusher , DogStatsDActionOwned } ;
250
+ use crate :: { create_client, new , DogStatsDActionOwned } ;
233
251
#[ cfg( unix) ]
234
252
use ddcommon:: connector:: uds:: socket_path_to_uri;
235
253
use ddcommon:: { tag, Endpoint } ;
236
254
#[ cfg( unix) ]
237
255
use http:: Uri ;
238
256
use std:: net;
257
+ use std:: sync:: Arc ;
239
258
use std:: time:: Duration ;
240
259
241
260
#[ test]
@@ -244,7 +263,7 @@ mod test {
244
263
let socket = net:: UdpSocket :: bind ( "127.0.0.1:0" ) . expect ( "failed to bind host socket" ) ;
245
264
let _ = socket. set_read_timeout ( Some ( Duration :: from_millis ( 500 ) ) ) ;
246
265
247
- let flusher = new_flusher ( Endpoint :: from_slice (
266
+ let flusher = new ( Endpoint :: from_slice (
248
267
socket. local_addr ( ) . unwrap ( ) . to_string ( ) . as_str ( ) ,
249
268
) )
250
269
. unwrap ( ) ;
@@ -333,12 +352,54 @@ mod test {
333
352
Histogram ( _, _, _) => { }
334
353
Set ( _, _, _) => { }
335
354
}
336
-
337
355
// TODO: when std::mem::variant_count is in stable we can do this instead
338
356
// assert_eq!(
339
357
// std::mem::variant_count::<DogStatsDActionOwned>(),
340
358
// std::mem::variant_count::<DogStatsDAction<String, Vec<&Tag>>>(),
341
359
// "DogStatsDActionOwned and DogStatsDAction should have the same number of variants,
342
360
// did you forget to update one?", );
343
361
}
362
+
363
+ #[ tokio:: test]
364
+ async fn test_thread_safety ( ) {
365
+ let socket = net:: UdpSocket :: bind ( "127.0.0.1:0" ) . expect ( "failed to bind host socket" ) ;
366
+ let _ = socket. set_read_timeout ( Some ( Duration :: from_millis ( 500 ) ) ) ;
367
+ let endpoint = Endpoint :: from_slice ( socket. local_addr ( ) . unwrap ( ) . to_string ( ) . as_str ( ) ) ;
368
+ let flusher = Arc :: new ( new ( endpoint. clone ( ) ) . unwrap ( ) ) ;
369
+
370
+ {
371
+ let client = flusher
372
+ . client
373
+ . lock ( )
374
+ . expect ( "failed to obtain lock on client" ) ;
375
+ assert ! ( client. is_none( ) ) ;
376
+ }
377
+
378
+ let tasks: Vec < _ > = ( 0 ..10 )
379
+ . map ( |_| {
380
+ let flusher_clone = Arc :: clone ( & flusher) ;
381
+ tokio:: spawn ( async move {
382
+ flusher_clone. send ( vec ! [
383
+ Count ( "test_count" , 3 , & vec![ tag!( "foo" , "bar" ) ] ) ,
384
+ Count ( "test_neg_count" , -2 , & vec![ ] ) ,
385
+ Distribution ( "test_distribution" , 4.2 , & vec![ ] ) ,
386
+ Gauge ( "test_gauge" , 7.6 , & vec![ ] ) ,
387
+ Histogram ( "test_histogram" , 8.0 , & vec![ ] ) ,
388
+ Set ( "test_set" , 9 , & vec![ tag!( "the" , "end" ) ] ) ,
389
+ Set ( "test_neg_set" , -1 , & vec![ ] ) ,
390
+ ] ) ;
391
+
392
+ let client = flusher_clone
393
+ . client
394
+ . lock ( )
395
+ . expect ( "failed to obtain lock on client within send thread" ) ;
396
+ assert ! ( client. is_some( ) ) ;
397
+ } )
398
+ } )
399
+ . collect ( ) ;
400
+
401
+ for task in tasks {
402
+ task. await . unwrap ( ) ;
403
+ }
404
+ }
344
405
}
0 commit comments