@@ -63,6 +63,9 @@ type PgxLeader struct {
6363 // lead() goroutine.
6464 unhealthySince time.Time
6565
66+ onIntegrityResult func (err error )
67+ onPingResult func (success bool , t time.Time )
68+
6669 // now returns the current time; overridable in tests. Use timeNow().
6770 now func () time.Time
6871
@@ -90,25 +93,38 @@ type PgxLeaderConfig struct {
9093 SweepInterval time.Duration
9194 MaxPingAge time.Duration
9295 StepDownTimeout time.Duration
96+ // OnIntegrityResult, if set, is called with the result of each cluster
97+ // integrity evaluation (nil means healthy). It may run concurrently with
98+ // OnPingResult, so it must be non-blocking, panic-free, and safe for
99+ // concurrent use.
100+ OnIntegrityResult func (err error )
101+ // OnPingResult, if set, is called after each leader ping attempt with
102+ // whether it succeeded and the time it ran. Periodic pings are dispatched on
103+ // their own goroutines, so it may run concurrently with itself and with
104+ // OnIntegrityResult; it must be non-blocking, panic-free, and safe for
105+ // concurrent use.
106+ OnPingResult func (success bool , t time.Time )
93107}
94108
95109func NewPgxLeader (cfg PgxLeaderConfig ) * PgxLeader {
96110 return & PgxLeader {
97- store : cfg .Store ,
98- awb : cfg .Broadcaster ,
99- notify : cfg .Notifier ,
100- taskHandler : cfg .TaskHandler ,
101- chLeader : cfg .LeaderChannel ,
102- chFollower : cfg .FollowerChannel ,
103- chMessages : cfg .MessagesChannel ,
104- address : cfg .Address ,
105- stop : cfg .StopChan ,
106- ping : cfg .PingInterval ,
107- sweep : cfg .SweepInterval ,
108- maxPingAge : cfg .MaxPingAge ,
109- stepDownTimeout : cfg .StepDownTimeout ,
110- now : time .Now ,
111- mutex : sync.RWMutex {},
111+ store : cfg .Store ,
112+ awb : cfg .Broadcaster ,
113+ notify : cfg .Notifier ,
114+ taskHandler : cfg .TaskHandler ,
115+ chLeader : cfg .LeaderChannel ,
116+ chFollower : cfg .FollowerChannel ,
117+ chMessages : cfg .MessagesChannel ,
118+ address : cfg .Address ,
119+ stop : cfg .StopChan ,
120+ ping : cfg .PingInterval ,
121+ sweep : cfg .SweepInterval ,
122+ maxPingAge : cfg .MaxPingAge ,
123+ stepDownTimeout : cfg .StepDownTimeout ,
124+ onIntegrityResult : cfg .OnIntegrityResult ,
125+ onPingResult : cfg .OnPingResult ,
126+ now : time .Now ,
127+ mutex : sync.RWMutex {},
112128 }
113129}
114130
@@ -276,6 +292,9 @@ func (p *PgxLeader) clusterIntegrityErr() error {
276292// (when that timeout is non-zero). Called from the lead() goroutine only.
277293func (p * PgxLeader ) evaluateHealth () bool {
278294 err := p .clusterIntegrityErr ()
295+ if p .onIntegrityResult != nil {
296+ p .onIntegrityResult (err )
297+ }
279298 if err == nil {
280299 if ! p .unhealthySince .IsZero () {
281300 slog .Info ("Cluster integrity recovered" , "degradedFor" , p .timeNow ().Sub (p .unhealthySince ))
@@ -320,6 +339,20 @@ func (p *PgxLeader) verify(vCh chan bool) {
320339// pingNodes sends a ping request out on the follower channel. All online cluster
321340// nodes should receive this message and respond with a ping response.
322341func (p * PgxLeader ) pingNodes (ctx context.Context ) {
342+ success := p .sendPings (ctx )
343+
344+ p .pingSuccessLock .Lock ()
345+ p .pingSuccess = success
346+ p .pingSuccessLock .Unlock ()
347+
348+ if p .onPingResult != nil {
349+ p .onPingResult (success , p .timeNow ())
350+ }
351+ }
352+
353+ // sendPings publishes the ping notifications on the follower and leader
354+ // channels and reports whether both sends succeeded.
355+ func (p * PgxLeader ) sendPings (ctx context.Context ) bool {
323356 req := & electiontypes.ClusterNotification {
324357 GuidVal : uuid .New ().String (),
325358 MessageType : electiontypes .ClusterMessageTypePing ,
@@ -328,30 +361,23 @@ func (p *PgxLeader) pingNodes(ctx context.Context) {
328361 b , err := json .Marshal (req )
329362 if err != nil {
330363 slog .Error ("Error marshaling notification to JSON" , "error" , err )
331- return
364+ return false
332365 }
333366
334- p .pingSuccessLock .Lock ()
335- defer p .pingSuccessLock .Unlock ()
336- p .pingSuccess = true
337-
338367 slog .Log (ctx , LevelTrace , fmt .Sprintf ("Leader pinging nodes on follower channel %s" , p .chFollower ))
339- err = p .notify .Notify (ctx , p .chFollower , b )
340- if err != nil {
341- p .pingSuccess = false
368+ if err = p .notify .Notify (ctx , p .chFollower , b ); err != nil {
342369 slog .Error ("Leader error pinging followers" , "error" , err )
343- return
370+ return false
344371 }
345372
346373 // This will ensure that the leader is tracked as part of the nodes in the cluster, and it will force
347374 // duplicate leaders to surrender the position.
348375 slog .Log (ctx , LevelTrace , fmt .Sprintf ("Leader pinging itself on leader channel %s" , p .chLeader ))
349- err = p .notify .Notify (ctx , p .chLeader , b )
350- if err != nil {
351- p .pingSuccess = false
376+ if err = p .notify .Notify (ctx , p .chLeader , b ); err != nil {
352377 slog .Error ("Leader error pinging leaders" , "error" , err )
353- return
378+ return false
354379 }
380+ return true
355381}
356382
357383func (p * PgxLeader ) handleNodesRequest (ctx context.Context , cn * electiontypes.ClusterNodesRequest ) {
0 commit comments