@@ -3,6 +3,8 @@ use crate::store::core::StoreContext;
3
3
use crate :: utils:: loop_heartbeats:: LoopHeartbeats ;
4
4
use anyhow:: Ok ;
5
5
use log:: { debug, error, info} ;
6
+ use reqwest:: Client ;
7
+ use serde_json:: json;
6
8
use shared:: web3:: contracts:: core:: builder:: Contracts ;
7
9
use std:: result:: Result ;
8
10
use std:: sync:: Arc ;
@@ -17,9 +19,12 @@ pub struct NodeStatusUpdater {
17
19
pool_id : u32 ,
18
20
disable_ejection : bool ,
19
21
heartbeats : Arc < LoopHeartbeats > ,
22
+ webhooks : Vec < String > ,
23
+ http_client : Client ,
20
24
}
21
25
22
26
impl NodeStatusUpdater {
27
+ #[ allow( clippy:: too_many_arguments) ]
23
28
pub fn new (
24
29
store_context : Arc < StoreContext > ,
25
30
update_interval : u64 ,
@@ -28,6 +33,7 @@ impl NodeStatusUpdater {
28
33
pool_id : u32 ,
29
34
disable_ejection : bool ,
30
35
heartbeats : Arc < LoopHeartbeats > ,
36
+ webhooks : Vec < String > ,
31
37
) -> Self {
32
38
Self {
33
39
store_context,
@@ -37,6 +43,8 @@ impl NodeStatusUpdater {
37
43
pool_id,
38
44
disable_ejection,
39
45
heartbeats,
46
+ webhooks,
47
+ http_client : Client :: new ( ) ,
40
48
}
41
49
}
42
50
@@ -108,6 +116,55 @@ impl NodeStatusUpdater {
108
116
Ok ( ( ) )
109
117
}
110
118
119
+ async fn trigger_webhooks (
120
+ & self ,
121
+ node : & OrchestratorNode ,
122
+ old_status : NodeStatus ,
123
+ ) -> Result < ( ) , anyhow:: Error > {
124
+ if old_status == node. status
125
+ || node. status == NodeStatus :: Unhealthy
126
+ || node. status == NodeStatus :: Discovered
127
+ {
128
+ return Ok ( ( ) ) ;
129
+ }
130
+
131
+ // If no webhooks configured, return early
132
+ if self . webhooks . is_empty ( ) {
133
+ return Ok ( ( ) ) ;
134
+ }
135
+
136
+ let payload = json ! ( {
137
+ "node_address" : node. address. to_string( ) ,
138
+ "ip_address" : node. ip_address,
139
+ "port" : node. port,
140
+ "old_status" : old_status. to_string( ) ,
141
+ "new_status" : node. status. to_string( ) ,
142
+ "timestamp" : chrono:: Utc :: now( ) . to_rfc3339( ) ,
143
+ } ) ;
144
+
145
+ let webhooks = self . webhooks . clone ( ) ;
146
+ let client = self . http_client . clone ( ) ;
147
+ tokio:: spawn ( async move {
148
+ for webhook_url in webhooks {
149
+ if let Err ( e) = client
150
+ . post ( & webhook_url)
151
+ . json ( & payload)
152
+ . timeout ( Duration :: from_secs ( 5 ) ) // Add timeout to prevent hanging
153
+ . send ( )
154
+ . await
155
+ {
156
+ error ! ( "Failed to send webhook to {}: {}" , webhook_url, e) ;
157
+ } else {
158
+ debug ! ( "Webhook to {} triggered successfully" , webhook_url) ;
159
+ }
160
+ }
161
+ } ) ;
162
+
163
+ tokio:: time:: sleep ( Duration :: from_millis ( 50 ) ) . await ;
164
+
165
+ Ok ( ( ) )
166
+ }
167
+
111
168
pub async fn sync_chain_with_nodes ( & self ) -> Result < ( ) , anyhow:: Error > {
112
169
let nodes = self . store_context . node_store . get_nodes ( ) ;
113
170
for node in nodes {
@@ -134,7 +191,8 @@ impl NodeStatusUpdater {
134
191
pub async fn process_nodes ( & self ) -> Result < ( ) , anyhow:: Error > {
135
192
let nodes = self . store_context . node_store . get_nodes ( ) ;
136
193
for node in nodes {
137
- let mut node = node. clone ( ) ;
194
+ let node = node. clone ( ) ;
195
+ let old_status = node. status . clone ( ) ;
138
196
let heartbeat = self
139
197
. store_context
140
198
. heartbeat_store
@@ -145,6 +203,9 @@ impl NodeStatusUpdater {
145
203
. get_unhealthy_counter ( & node. address ) ;
146
204
147
205
let is_node_in_pool = self . is_node_in_pool ( & node) . await ;
206
+ let mut status_changed = false ;
207
+ let mut new_status = node. status . clone ( ) ;
208
+
148
209
match heartbeat {
149
210
Some ( beat) => {
150
211
// Update version if necessary
@@ -164,29 +225,23 @@ impl NodeStatusUpdater {
164
225
|| node. status == NodeStatus :: WaitingForHeartbeat
165
226
{
166
227
if is_node_in_pool {
167
- node . status = NodeStatus :: Healthy ;
228
+ new_status = NodeStatus :: Healthy ;
168
229
} else {
169
230
// Reset to discovered to init re-invite to pool
170
- node . status = NodeStatus :: Discovered ;
231
+ new_status = NodeStatus :: Discovered ;
171
232
}
172
- let _: ( ) = self
173
- . store_context
174
- . node_store
175
- . update_node_status ( & node. address , node. status ) ;
233
+ status_changed = true ;
176
234
}
177
235
// If node is Discovered or Dead:
178
236
else if node. status == NodeStatus :: Discovered
179
237
|| node. status == NodeStatus :: Dead
180
238
{
181
239
if is_node_in_pool {
182
- node . status = NodeStatus :: Healthy ;
240
+ new_status = NodeStatus :: Healthy ;
183
241
} else {
184
- node . status = NodeStatus :: Discovered ;
242
+ new_status = NodeStatus :: Discovered ;
185
243
}
186
- let _: ( ) = self
187
- . store_context
188
- . node_store
189
- . update_node_status ( & node. address , node. status ) ;
244
+ status_changed = true ;
190
245
}
191
246
192
247
// Clear unhealthy counter on heartbeat receipt
@@ -203,15 +258,13 @@ impl NodeStatusUpdater {
203
258
204
259
match node. status {
205
260
NodeStatus :: Healthy => {
206
- self . store_context
207
- . node_store
208
- . update_node_status ( & node. address , NodeStatus :: Unhealthy ) ;
261
+ new_status = NodeStatus :: Unhealthy ;
262
+ status_changed = true ;
209
263
}
210
264
NodeStatus :: Unhealthy => {
211
265
if unhealthy_counter + 1 >= self . missing_heartbeat_threshold {
212
- self . store_context
213
- . node_store
214
- . update_node_status ( & node. address , NodeStatus :: Dead ) ;
266
+ new_status = NodeStatus :: Dead ;
267
+ status_changed = true ;
215
268
}
216
269
}
217
270
NodeStatus :: Discovered => {
@@ -220,24 +273,33 @@ impl NodeStatusUpdater {
220
273
// The node is in pool but does not send heartbeats - maybe due to a downtime of the orchestrator?
221
274
// Node invites fail now since the node cannot be in pool again.
222
275
// We have to eject and re-invite - we can simply do this by setting the status to unhealthy. The node will eventually be ejected.
223
- self . store_context
224
- . node_store
225
- . update_node_status ( & node. address , NodeStatus :: Unhealthy ) ;
276
+ new_status = NodeStatus :: Unhealthy ;
277
+ status_changed = true ;
226
278
}
227
279
}
228
280
NodeStatus :: WaitingForHeartbeat => {
229
281
if unhealthy_counter + 1 >= self . missing_heartbeat_threshold {
230
282
// Unhealthy counter is reset when node is invited
231
283
// usually it starts directly with heartbeat
232
- self . store_context
233
- . node_store
234
- . update_node_status ( & node. address , NodeStatus :: Unhealthy ) ;
284
+ new_status = NodeStatus :: Unhealthy ;
285
+ status_changed = true ;
235
286
}
236
287
}
237
288
_ => ( ) ,
238
289
}
239
290
}
240
291
}
292
+
293
+ if status_changed {
294
+ let _: ( ) = self
295
+ . store_context
296
+ . node_store
297
+ . update_node_status ( & node. address , new_status) ;
298
+
299
+ if let Some ( updated_node) = self . store_context . node_store . get_node ( & node. address ) {
300
+ let _ = self . trigger_webhooks ( & updated_node, old_status) . await ;
301
+ }
302
+ }
241
303
}
242
304
Ok ( ( ) )
243
305
}
@@ -269,6 +331,7 @@ mod tests {
269
331
0 ,
270
332
false ,
271
333
Arc :: new ( LoopHeartbeats :: new ( ) ) ,
334
+ vec ! [ ] ,
272
335
) ;
273
336
let node = OrchestratorNode {
274
337
address : Address :: from_str ( "0x0000000000000000000000000000000000000000" ) . unwrap ( ) ,
@@ -342,6 +405,7 @@ mod tests {
342
405
0 ,
343
406
false ,
344
407
Arc :: new ( LoopHeartbeats :: new ( ) ) ,
408
+ vec ! [ ] ,
345
409
) ;
346
410
tokio:: spawn ( async move {
347
411
updater
@@ -386,6 +450,7 @@ mod tests {
386
450
0 ,
387
451
false ,
388
452
Arc :: new ( LoopHeartbeats :: new ( ) ) ,
453
+ vec ! [ ] ,
389
454
) ;
390
455
tokio:: spawn ( async move {
391
456
updater
@@ -440,6 +505,7 @@ mod tests {
440
505
0 ,
441
506
false ,
442
507
Arc :: new ( LoopHeartbeats :: new ( ) ) ,
508
+ vec ! [ ] ,
443
509
) ;
444
510
tokio:: spawn ( async move {
445
511
updater
@@ -498,6 +564,7 @@ mod tests {
498
564
0 ,
499
565
false ,
500
566
Arc :: new ( LoopHeartbeats :: new ( ) ) ,
567
+ vec ! [ ] ,
501
568
) ;
502
569
tokio:: spawn ( async move {
503
570
updater
@@ -564,6 +631,7 @@ mod tests {
564
631
0 ,
565
632
false ,
566
633
Arc :: new ( LoopHeartbeats :: new ( ) ) ,
634
+ vec ! [ ] ,
567
635
) ;
568
636
tokio:: spawn ( async move {
569
637
updater
@@ -629,6 +697,7 @@ mod tests {
629
697
0 ,
630
698
false ,
631
699
Arc :: new ( LoopHeartbeats :: new ( ) ) ,
700
+ vec ! [ ] ,
632
701
) ;
633
702
tokio:: spawn ( async move {
634
703
updater
@@ -681,7 +750,6 @@ mod tests {
681
750
version : None ,
682
751
last_status_change : None ,
683
752
} ;
684
- println ! ( "Node: {:?}" , node) ;
685
753
686
754
let _: ( ) = app_state. store_context . node_store . add_node ( node. clone ( ) ) ;
687
755
let updater = NodeStatusUpdater :: new (
@@ -692,6 +760,7 @@ mod tests {
692
760
0 ,
693
761
false ,
694
762
Arc :: new ( LoopHeartbeats :: new ( ) ) ,
763
+ vec ! [ ] ,
695
764
) ;
696
765
tokio:: spawn ( async move {
697
766
updater
@@ -748,6 +817,7 @@ mod tests {
748
817
0 ,
749
818
false ,
750
819
Arc :: new ( LoopHeartbeats :: new ( ) ) ,
820
+ vec ! [ ] ,
751
821
) ;
752
822
tokio:: spawn ( async move {
753
823
updater
0 commit comments