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 }