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  }