github.com/asynkron/protoactor-go@v0.0.0-20240308120642-ef91a6abee75/cluster/clusterproviders/test/test_provider.go (about)

     1  package test
     2  
     3  import (
     4  	"log/slog"
     5  	"sync"
     6  	"time"
     7  
     8  	"github.com/asynkron/protoactor-go/cluster"
     9  	"golang.org/x/exp/maps"
    10  )
    11  
    12  type ProviderConfig struct {
    13  	// ServiceTtl is the time to live for services. Default: 3s
    14  	ServiceTtl time.Duration
    15  	// RefreshTtl is the time between refreshes of the service ttl. Default: 1s
    16  	RefreshTtl time.Duration
    17  	// DeregisterCritical is the time after which a service is deregistered if it is not refreshed. Default: 10s
    18  	DeregisterCritical time.Duration
    19  }
    20  
    21  type ProviderOption func(config *ProviderConfig)
    22  
    23  // WithTestProviderServiceTtl sets the service ttl. Default: 3s
    24  func WithTestProviderServiceTtl(serviceTtl time.Duration) ProviderOption {
    25  	return func(config *ProviderConfig) {
    26  		config.ServiceTtl = serviceTtl
    27  	}
    28  }
    29  
    30  // WithTestProviderRefreshTtl sets the refresh ttl. Default: 1s
    31  func WithTestProviderRefreshTtl(refreshTtl time.Duration) ProviderOption {
    32  	return func(config *ProviderConfig) {
    33  		config.RefreshTtl = refreshTtl
    34  	}
    35  }
    36  
    37  // WithTestProviderDeregisterCritical sets the deregister critical. Default: 10s
    38  func WithTestProviderDeregisterCritical(deregisterCritical time.Duration) ProviderOption {
    39  	return func(config *ProviderConfig) {
    40  		config.DeregisterCritical = deregisterCritical
    41  	}
    42  }
    43  
    44  type Provider struct {
    45  	memberList *cluster.MemberList
    46  	config     *ProviderConfig
    47  
    48  	agent           *InMemAgent
    49  	id              string
    50  	ttlReportTicker *time.Ticker
    51  	cluster         *cluster.Cluster
    52  }
    53  
    54  func NewTestProvider(agent *InMemAgent, options ...ProviderOption) *Provider {
    55  	config := &ProviderConfig{
    56  		ServiceTtl:         time.Second * 3,
    57  		RefreshTtl:         time.Second,
    58  		DeregisterCritical: time.Second * 10,
    59  	}
    60  	for _, option := range options {
    61  		option(config)
    62  	}
    63  	return &Provider{
    64  		config: config,
    65  		agent:  agent,
    66  	}
    67  }
    68  
    69  func (t *Provider) StartMember(c *cluster.Cluster) error {
    70  
    71  	c.ActorSystem.Logger().Debug("start cluster member")
    72  	t.memberList = c.MemberList
    73  	host, port, err := c.ActorSystem.GetHostPort()
    74  	if err != nil {
    75  		return err
    76  	}
    77  	kinds := c.GetClusterKinds()
    78  	t.cluster = c
    79  	t.id = c.ActorSystem.ID
    80  	t.startTtlReport()
    81  	t.agent.SubscribeStatusUpdate(t.notifyStatuses)
    82  	t.agent.RegisterService(NewAgentServiceStatus(t.id, host, port, kinds))
    83  	return nil
    84  }
    85  
    86  func (t *Provider) StartClient(cluster *cluster.Cluster) error {
    87  	t.memberList = cluster.MemberList
    88  	t.id = cluster.ActorSystem.ID
    89  	t.agent.SubscribeStatusUpdate(t.notifyStatuses)
    90  	t.agent.ForceUpdate()
    91  	return nil
    92  }
    93  
    94  func (t *Provider) Shutdown(_ bool) error {
    95  	t.cluster.Logger().Debug("Unregistering service", slog.String("service", t.id))
    96  	if t.ttlReportTicker != nil {
    97  		t.ttlReportTicker.Stop()
    98  	}
    99  	t.agent.DeregisterService(t.id)
   100  	return nil
   101  }
   102  
   103  // notifyStatuses notifies the cluster that the service status has changed.
   104  func (t *Provider) notifyStatuses() {
   105  	statuses := t.agent.GetStatusHealth()
   106  
   107  	t.cluster.Logger().Debug("TestAgent response", slog.Any("statuses", statuses))
   108  	members := make([]*cluster.Member, 0, len(statuses))
   109  	for _, status := range statuses {
   110  		copiedKinds := make([]string, 0, len(status.Kinds))
   111  		copiedKinds = append(copiedKinds, status.Kinds...)
   112  
   113  		members = append(members, &cluster.Member{
   114  			Id:    status.ID,
   115  			Port:  int32(status.Port),
   116  			Host:  status.Host,
   117  			Kinds: copiedKinds,
   118  		})
   119  	}
   120  	t.memberList.UpdateClusterTopology(members)
   121  }
   122  
   123  // startTtlReport starts the ttl report loop.
   124  func (t *Provider) startTtlReport() {
   125  	t.ttlReportTicker = time.NewTicker(t.config.RefreshTtl)
   126  	go func() {
   127  		for range t.ttlReportTicker.C {
   128  			t.agent.RefreshServiceTTL(t.id)
   129  		}
   130  	}()
   131  }
   132  
   133  type InMemAgent struct {
   134  	services     map[string]AgentServiceStatus
   135  	servicesLock *sync.RWMutex
   136  
   137  	statusUpdateHandlers     []func()
   138  	statusUpdateHandlersLock *sync.RWMutex
   139  }
   140  
   141  func NewInMemAgent() *InMemAgent {
   142  	return &InMemAgent{
   143  		services:                 make(map[string]AgentServiceStatus),
   144  		servicesLock:             &sync.RWMutex{},
   145  		statusUpdateHandlers:     make([]func(), 0),
   146  		statusUpdateHandlersLock: &sync.RWMutex{},
   147  	}
   148  }
   149  
   150  // RegisterService registers a AgentServiceStatus with the agent.
   151  func (m *InMemAgent) RegisterService(registration AgentServiceStatus) {
   152  	m.servicesLock.Lock()
   153  	m.services[registration.ID] = registration
   154  	m.servicesLock.Unlock()
   155  
   156  	m.onStatusUpdate()
   157  }
   158  
   159  // DeregisterService removes a service from the agent.
   160  func (m *InMemAgent) DeregisterService(id string) {
   161  	m.servicesLock.Lock()
   162  	delete(m.services, id)
   163  	m.servicesLock.Unlock()
   164  
   165  	m.onStatusUpdate()
   166  }
   167  
   168  // RefreshServiceTTL updates the TTL of all services.
   169  func (m *InMemAgent) RefreshServiceTTL(id string) {
   170  	m.servicesLock.Lock()
   171  	defer m.servicesLock.Unlock()
   172  	if service, ok := m.services[id]; ok {
   173  		service.TTL = time.Now()
   174  		m.services[id] = service
   175  	}
   176  }
   177  
   178  // SubscribeStatusUpdate registers a handler that will be called when the service map changes.
   179  func (m *InMemAgent) SubscribeStatusUpdate(handler func()) {
   180  	m.statusUpdateHandlersLock.Lock()
   181  	defer m.statusUpdateHandlersLock.Unlock()
   182  	m.statusUpdateHandlers = append(m.statusUpdateHandlers, handler)
   183  }
   184  
   185  // GetStatusHealth returns the health of the service.
   186  func (m *InMemAgent) GetStatusHealth() []AgentServiceStatus {
   187  	m.servicesLock.RLock()
   188  	defer m.servicesLock.RUnlock()
   189  	return maps.Values(m.services)
   190  }
   191  
   192  // ForceUpdate is used to trigger a status update event.
   193  func (m *InMemAgent) ForceUpdate() {
   194  	m.onStatusUpdate()
   195  }
   196  
   197  func (m *InMemAgent) onStatusUpdate() {
   198  	m.statusUpdateHandlersLock.RLock()
   199  	defer m.statusUpdateHandlersLock.RUnlock()
   200  	for _, handler := range m.statusUpdateHandlers {
   201  		handler()
   202  	}
   203  }
   204  
   205  type AgentServiceStatus struct {
   206  	ID    string
   207  	TTL   time.Time // last alive time
   208  	Host  string
   209  	Port  int
   210  	Kinds []string
   211  }
   212  
   213  // NewAgentServiceStatus creates a new AgentServiceStatus.
   214  func NewAgentServiceStatus(id string, host string, port int, kinds []string) AgentServiceStatus {
   215  	return AgentServiceStatus{
   216  		ID:    id,
   217  		TTL:   time.Now(),
   218  		Host:  host,
   219  		Port:  port,
   220  		Kinds: kinds,
   221  	}
   222  }
   223  
   224  func (a AgentServiceStatus) Alive() bool {
   225  	return time.Now().Sub(a.TTL) <= (time.Second * 5)
   226  }