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  }