github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/providers/agent/mcorpc/client/stats.go (about) 1 // Copyright (c) 2020-2021, R.I. Pienaar and the Choria Project contributors 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package client 6 7 import ( 8 "fmt" 9 "sync" 10 "sync/atomic" 11 "time" 12 ) 13 14 // Stats represent stats for a request 15 type Stats struct { 16 RequestID string 17 18 discoveredNodes []string 19 20 outstandingNodes *NodeList 21 unexpectedRespones *NodeList 22 23 responses int32 24 passed int32 25 failed int32 26 27 start time.Time 28 end time.Time 29 30 publishStart time.Time 31 publishEnd time.Time 32 publishTotal time.Duration 33 publishing bool 34 35 discoveryStart time.Time 36 discoveryEnd time.Time 37 38 agent string 39 action string 40 41 mu *sync.Mutex 42 } 43 44 // NewStats initializes a new stats instance 45 func NewStats() *Stats { 46 return &Stats{ 47 discoveredNodes: []string{}, 48 outstandingNodes: NewNodeList(), 49 unexpectedRespones: NewNodeList(), 50 mu: &sync.Mutex{}, 51 } 52 } 53 54 // Merge merges the stats from a specific batch into this 55 func (s *Stats) Merge(other *Stats) error { 56 if other.All() { 57 for _, n := range other.discoveredNodes { 58 s.RecordReceived(n) 59 } 60 } else { 61 for _, n := range other.discoveredNodes { 62 if !other.outstandingNodes.Have(n) { 63 s.RecordReceived(n) 64 } 65 } 66 } 67 68 s.unexpectedRespones.AddHosts(other.UnexpectedResponseFrom()...) 69 70 atomic.AddInt32(&s.passed, other.passed) 71 atomic.AddInt32(&s.failed, other.failed) 72 73 d, err := other.PublishDuration() 74 if err != nil { 75 return err 76 } 77 78 s.mu.Lock() 79 s.publishTotal += d 80 s.mu.Unlock() 81 82 return nil 83 } 84 85 // SetAgent stores the agent the stats is for 86 func (s *Stats) SetAgent(a string) { 87 s.agent = a 88 } 89 90 // SetAction stores the action the stats is for 91 func (s *Stats) SetAction(a string) { 92 s.action = a 93 } 94 95 // Agent returns the agent the stat is for if it was set 96 func (s *Stats) Agent() string { 97 return s.agent 98 } 99 100 // Action returns the action the stat is for if it was set 101 func (s *Stats) Action() string { 102 return s.action 103 } 104 105 // All determines if all expected nodes replied already 106 func (s *Stats) All() bool { 107 return s.outstandingNodes.Count() == 0 108 } 109 110 // NoResponseFrom calculates discovered which hosts did not respond 111 func (s *Stats) NoResponseFrom() []string { 112 return s.outstandingNodes.Hosts() 113 } 114 115 // UnexpectedResponseFrom calculates which hosts responses that we did not expect responses from 116 func (s *Stats) UnexpectedResponseFrom() []string { 117 return s.unexpectedRespones.Hosts() 118 } 119 120 // WaitingFor checks if any of the given nodes are still outstanding 121 func (s *Stats) WaitingFor(nodes []string) bool { 122 s.mu.Lock() 123 defer s.mu.Unlock() 124 125 return s.outstandingNodes.HaveAny(nodes...) 126 } 127 128 // SetDiscoveredNodes records the node names we expect to communicate with 129 func (s *Stats) SetDiscoveredNodes(nodes []string) { 130 s.mu.Lock() 131 defer s.mu.Unlock() 132 133 s.discoveredNodes = nodes 134 135 s.outstandingNodes.Clear() 136 s.outstandingNodes.AddHosts(nodes...) 137 } 138 139 // FailedRequestInc increments the failed request counter by one 140 func (s *Stats) FailedRequestInc() { 141 atomic.AddInt32(&s.failed, 1) 142 } 143 144 // PassedRequestInc increments the passed request counter by one 145 func (s *Stats) PassedRequestInc() { 146 atomic.AddInt32(&s.passed, 1) 147 } 148 149 // RecordReceived reords the fact that one message was received 150 func (s *Stats) RecordReceived(sender string) { 151 s.mu.Lock() 152 defer s.mu.Unlock() 153 154 atomic.AddInt32(&s.responses, 1) 155 156 known := s.outstandingNodes.DeleteIfKnown(sender) 157 if !known { 158 s.unexpectedRespones.AddHosts(sender) 159 } 160 } 161 162 // DiscoveredCount is how many nodes were discovered 163 func (s *Stats) DiscoveredCount() int { 164 return len(s.discoveredNodes) 165 } 166 167 // DiscoveredNodes are the nodes that was discovered for this request 168 func (s *Stats) DiscoveredNodes() *[]string { 169 return &s.discoveredNodes 170 } 171 172 // FailCount is the number of responses that were failures 173 func (s *Stats) FailCount() int { 174 return int(atomic.LoadInt32(&s.failed)) 175 } 176 177 // OKCount is the number of responses that were ok 178 func (s *Stats) OKCount() int { 179 return int(atomic.LoadInt32(&s.passed)) 180 } 181 182 // ResponsesCount if the total amount of nodes that responded so far 183 func (s *Stats) ResponsesCount() int { 184 return int(atomic.LoadInt32(&s.responses)) 185 } 186 187 // StartPublish records the publish process started 188 func (s *Stats) StartPublish() { 189 s.mu.Lock() 190 defer s.mu.Unlock() 191 192 if s.publishing { 193 return 194 } 195 196 s.publishStart = time.Now() 197 s.publishing = true 198 } 199 200 // EndPublish records the publish process ended 201 func (s *Stats) EndPublish() { 202 s.mu.Lock() 203 defer s.mu.Unlock() 204 205 if !s.publishing { 206 return 207 } 208 209 s.publishEnd = time.Now() 210 s.publishing = false 211 212 s.publishTotal += s.publishEnd.Sub(s.publishStart) 213 } 214 215 // PublishDuration calculates how long publishing took 216 func (s *Stats) PublishDuration() (time.Duration, error) { 217 s.mu.Lock() 218 defer s.mu.Unlock() 219 220 if s.publishTotal == 0 || s.publishing { 221 return time.Duration(0), fmt.Errorf("publishing is not completed") 222 } 223 224 return s.publishTotal, nil 225 } 226 227 // RequestDuration calculates the total duration 228 func (s *Stats) RequestDuration() (time.Duration, error) { 229 s.mu.Lock() 230 defer s.mu.Unlock() 231 232 if s.start.IsZero() || s.end.IsZero() { 233 return time.Duration(0), fmt.Errorf("request is not completed") 234 } 235 236 return s.end.Sub(s.start), nil 237 } 238 239 // Start records the start time of a request 240 func (s *Stats) Start() { 241 s.mu.Lock() 242 defer s.mu.Unlock() 243 244 if s.start.IsZero() { 245 s.start = time.Now() 246 } 247 } 248 249 // Started is the time the request was started, zero time when not started 250 func (s *Stats) Started() time.Time { 251 s.mu.Lock() 252 defer s.mu.Unlock() 253 254 return s.start 255 } 256 257 // End records the end time of a request 258 func (s *Stats) End() { 259 s.mu.Lock() 260 defer s.mu.Unlock() 261 262 if s.end.IsZero() { 263 s.end = time.Now() 264 } 265 } 266 267 // StartDiscover records the start time of the discovery process 268 func (s *Stats) StartDiscover() { 269 s.mu.Lock() 270 defer s.mu.Unlock() 271 272 if s.discoveryStart.IsZero() { 273 s.discoveryStart = time.Now() 274 } 275 } 276 277 // EndDiscover records the end time of the discovery process 278 func (s *Stats) EndDiscover() { 279 s.mu.Lock() 280 defer s.mu.Unlock() 281 282 if s.discoveryEnd.IsZero() { 283 s.discoveryEnd = time.Now() 284 } 285 } 286 287 // OverrideDiscoveryTime sets specific discovery time 288 func (s *Stats) OverrideDiscoveryTime(start time.Time, end time.Time) { 289 s.mu.Lock() 290 defer s.mu.Unlock() 291 292 s.discoveryStart = start 293 s.discoveryEnd = end 294 } 295 296 // DiscoveryDuration determines how long discovery took, 0 and error when discovery was not done 297 func (s *Stats) DiscoveryDuration() (time.Duration, error) { 298 s.mu.Lock() 299 defer s.mu.Unlock() 300 301 if s.discoveryStart.IsZero() || s.discoveryEnd.IsZero() { 302 return time.Duration(0), fmt.Errorf("discovery was not performed") 303 } 304 305 return s.discoveryEnd.Sub(s.discoveryStart), nil 306 } 307 308 // UniqueRequestID is a unique identifier for the request, can be empty 309 func (s *Stats) UniqueRequestID() string { 310 s.mu.Lock() 311 defer s.mu.Unlock() 312 313 return s.RequestID 314 }