github.com/asynkron/protoactor-go@v0.0.0-20240308120642-ef91a6abee75/cluster/default_context.go (about) 1 // Copyright (C) 2017 - 2022 Asynkron.se <http://www.asynkron.se> 2 3 package cluster 4 5 import ( 6 "context" 7 "fmt" 8 "log/slog" 9 "time" 10 11 "github.com/asynkron/protoactor-go/actor" 12 "github.com/asynkron/protoactor-go/remote" 13 ) 14 15 // Defines a type to provide DefaultContext configurations / implementations. 16 type ContextProducer func(*Cluster) Context 17 18 // Defines a default cluster context hashBytes structure. 19 type DefaultContext struct { 20 cluster *Cluster 21 } 22 23 var _ Context = (*DefaultContext)(nil) 24 25 // Creates a new DefaultContext value and returns 26 // a pointer to its memory address as a Context. 27 func newDefaultClusterContext(cluster *Cluster) Context { 28 clusterContext := DefaultContext{ 29 cluster: cluster, 30 } 31 32 return &clusterContext 33 } 34 35 func (dcc *DefaultContext) Request(identity, kind string, message interface{}, opts ...GrainCallOption) (interface{}, error) { 36 var err error 37 38 var resp interface{} 39 40 var counter int 41 callConfig := DefaultGrainCallConfig(dcc.cluster) 42 for _, o := range opts { 43 o(callConfig) 44 } 45 46 _context := callConfig.Context 47 48 // get the configuration from the composed Cluster value 49 cfg := dcc.cluster.Config.ToClusterContextConfig(dcc.cluster.Logger()) 50 51 start := time.Now() 52 53 dcc.cluster.Logger().Debug(fmt.Sprintf("Requesting %s:%s Message %#v", identity, kind, message)) 54 55 // crate a new Timeout Context 56 ttl := callConfig.Timeout 57 58 ctx, cancel := context.WithTimeout(context.Background(), ttl) 59 defer cancel() 60 61 selectloop: 62 for { 63 select { 64 case <-ctx.Done(): 65 // TODO: handler throttling and messaging here 66 err = fmt.Errorf("request failed: %w", ctx.Err()) 67 68 break selectloop 69 default: 70 if counter >= callConfig.RetryCount { 71 err = fmt.Errorf("have reached max retries: %v", callConfig.RetryCount) 72 73 break selectloop 74 } 75 pid := dcc.getPid(identity, kind) 76 if pid == nil { 77 dcc.cluster.Logger().Debug("Requesting PID from IdentityLookup but got nil", slog.String("identity", identity), slog.String("kind", kind)) 78 counter = callConfig.RetryAction(counter) 79 continue 80 } 81 82 // TODO: why is err != nil when res != nil? 83 resp, err = _context.RequestFuture(pid, message, ttl).Result() 84 if resp != nil { 85 break selectloop 86 } 87 if err != nil { 88 dcc.cluster.Logger().Error("cluster.RequestFuture failed", slog.Any("error", err), slog.Any("pid", pid)) 89 switch err { 90 case actor.ErrTimeout, remote.ErrTimeout, actor.ErrDeadLetter, remote.ErrDeadLetter: 91 counter = callConfig.RetryAction(counter) 92 dcc.cluster.PidCache.Remove(identity, kind) 93 continue 94 default: 95 break selectloop 96 } 97 } 98 99 // TODO: add metrics to increment retries 100 } 101 } 102 103 totalTime := time.Since(start) 104 // TODO: add metrics ot set histogram for total request time 105 106 if contextError := ctx.Err(); contextError != nil && cfg.requestLogThrottle() == actor.Open { 107 // context timeout exceeded, report and return 108 dcc.cluster.Logger().Warn("Request retried but failed", slog.String("identity", identity), slog.String("kind", kind), slog.Duration("duration", totalTime)) 109 } 110 111 return resp, err 112 } 113 114 func (dcc *DefaultContext) RequestFuture(identity string, kind string, message interface{}, opts ...GrainCallOption) (*actor.Future, error) { 115 var counter int 116 callConfig := DefaultGrainCallConfig(dcc.cluster) 117 for _, o := range opts { 118 o(callConfig) 119 } 120 121 _context := callConfig.Context 122 123 dcc.cluster.Logger().Debug(fmt.Sprintf("Requesting future %s:%s Message %#v", identity, kind, message)) 124 125 // crate a new Timeout Context 126 ttl := callConfig.Timeout 127 128 ctx, cancel := context.WithTimeout(context.Background(), ttl) 129 defer cancel() 130 131 for { 132 select { 133 case <-ctx.Done(): 134 // TODO: handler throttling and messaging here 135 err := fmt.Errorf("request failed: %w", ctx.Err()) 136 return nil, err 137 default: 138 if counter >= callConfig.RetryCount { 139 return nil, fmt.Errorf("have reached max retries: %v", callConfig.RetryCount) 140 } 141 142 pid := dcc.getPid(identity, kind) 143 if pid == nil { 144 dcc.cluster.Logger().Debug("Requesting PID from IdentityLookup but got nil", slog.String("identity", identity), slog.String("kind", kind)) 145 counter = callConfig.RetryAction(counter) 146 continue 147 } 148 149 f := _context.RequestFuture(pid, message, ttl) 150 return f, nil 151 } 152 } 153 } 154 155 // gets the cached PID for the given identity 156 // it can return nil if none is found. 157 func (dcc *DefaultContext) getPid(identity, kind string) *actor.PID { 158 pid, _ := dcc.cluster.PidCache.Get(identity, kind) 159 if pid == nil { 160 pid = dcc.cluster.Get(identity, kind) 161 dcc.cluster.PidCache.Set(identity, kind, pid) 162 } 163 164 return pid 165 }