github.com/asynkron/protoactor-go@v0.0.0-20240308120642-ef91a6abee75/cluster/clusterproviders/consul/provider_actor.go (about) 1 package consul 2 3 import ( 4 "fmt" 5 "log/slog" 6 7 "github.com/asynkron/protoactor-go/actor" 8 "github.com/asynkron/protoactor-go/cluster" 9 "github.com/asynkron/protoactor-go/scheduler" 10 "github.com/hashicorp/consul/api" 11 "github.com/hashicorp/consul/api/watch" 12 ) 13 14 type providerActor struct { 15 *Provider 16 actor.Behavior 17 refreshCanceller scheduler.CancelFunc 18 } 19 20 type ( 21 RegisterService struct{} 22 UpdateTTL struct{} 23 MemberListUpdated struct { 24 members []*cluster.Member 25 index uint64 26 } 27 ) 28 29 func (pa *providerActor) Receive(ctx actor.Context) { 30 pa.Behavior.Receive(ctx) 31 } 32 33 func newProviderActor(provider *Provider) actor.Actor { 34 pa := &providerActor{ 35 Behavior: actor.NewBehavior(), 36 Provider: provider, 37 } 38 pa.Become(pa.init) 39 return pa 40 } 41 42 func (pa *providerActor) init(ctx actor.Context) { 43 switch ctx.Message().(type) { 44 case *actor.Started: 45 ctx.Send(ctx.Self(), &RegisterService{}) 46 case *RegisterService: 47 if err := pa.registerService(); err != nil { 48 ctx.Logger().Error("Failed to register service to consul, will retry", slog.Any("error", err)) 49 ctx.Send(ctx.Self(), &RegisterService{}) 50 } else { 51 ctx.Logger().Info("Registered service to consul") 52 refreshScheduler := scheduler.NewTimerScheduler(ctx) 53 pa.refreshCanceller = refreshScheduler.SendRepeatedly(0, pa.refreshTTL, ctx.Self(), &UpdateTTL{}) 54 if err := pa.startWatch(ctx); err == nil { 55 pa.Become(pa.running) 56 } 57 } 58 } 59 } 60 61 func (pa *providerActor) running(ctx actor.Context) { 62 switch msg := ctx.Message().(type) { 63 case *UpdateTTL: 64 if err := blockingUpdateTTL(pa.Provider); err != nil { 65 ctx.Logger().Warn("Failed to update TTL", slog.Any("error", err)) 66 } 67 case *MemberListUpdated: 68 pa.cluster.MemberList.UpdateClusterTopology(msg.members) 69 case *actor.Stopping: 70 pa.refreshCanceller() 71 if err := pa.deregisterService(); err != nil { 72 ctx.Logger().Error("Failed to deregister service from consul", slog.Any("error", err)) 73 } else { 74 ctx.Logger().Info("De-registered service from consul") 75 } 76 } 77 } 78 79 func (pa *providerActor) startWatch(ctx actor.Context) error { 80 params := make(map[string]interface{}) 81 params["type"] = "service" 82 params["service"] = pa.clusterName 83 params["passingonly"] = false 84 plan, err := watch.Parse(params) 85 if err != nil { 86 ctx.Logger().Error("Failed to parse consul watch definition", slog.Any("error", err)) 87 return err 88 } 89 plan.Handler = func(index uint64, result interface{}) { 90 pa.processConsulUpdate(index, result, ctx) 91 } 92 93 go func() { 94 if err = plan.RunWithConfig(pa.consulConfig.Address, pa.consulConfig); err != nil { 95 ctx.Logger().Error("Failed to start consul watch", slog.Any("error", err)) 96 panic(err) 97 } 98 }() 99 100 return nil 101 } 102 103 func (pa *providerActor) processConsulUpdate(index uint64, result interface{}, ctx actor.Context) { 104 serviceEntries, ok := result.([]*api.ServiceEntry) 105 if !ok { 106 ctx.Logger().Warn("Didn't get expected data from consul watch") 107 return 108 } 109 var members []*cluster.Member 110 for _, v := range serviceEntries { 111 if len(v.Checks) > 0 && v.Checks.AggregatedStatus() == api.HealthPassing { 112 memberId := v.Service.Meta["id"] 113 if memberId == "" { 114 memberId = fmt.Sprintf("%v@%v:%v", pa.clusterName, v.Service.Address, v.Service.Port) 115 ctx.Logger().Info("meta['id'] was empty, fixed", slog.String("id", memberId)) 116 } 117 members = append(members, &cluster.Member{ 118 Id: memberId, 119 Host: v.Service.Address, 120 Port: int32(v.Service.Port), 121 Kinds: v.Service.Tags, 122 }) 123 } 124 } 125 126 // delay the fist update until there is at least one member 127 if len(members) > 0 { 128 ctx.Send(ctx.Self(), &MemberListUpdated{ 129 members: members, 130 index: index, 131 }) 132 } 133 }