github.com/asynkron/protoactor-go@v0.0.0-20240308120642-ef91a6abee75/remote/endpoint_writer.go (about) 1 package remote 2 3 import ( 4 "errors" 5 "io" 6 "log/slog" 7 "time" 8 9 "github.com/asynkron/protoactor-go/actor" 10 "golang.org/x/net/context" 11 "google.golang.org/grpc" 12 "google.golang.org/protobuf/proto" 13 ) 14 15 func endpointWriterProducer(remote *Remote, address string, config *Config) actor.Producer { 16 return func() actor.Actor { 17 return &endpointWriter{ 18 address: address, 19 config: config, 20 remote: remote, 21 } 22 } 23 } 24 25 type endpointWriter struct { 26 config *Config 27 address string 28 conn *grpc.ClientConn 29 stream Remoting_ReceiveClient 30 remote *Remote 31 } 32 33 type restartAfterConnectFailure struct { 34 err error 35 } 36 37 func (state *endpointWriter) initialize(ctx actor.Context) { 38 now := time.Now() 39 40 state.remote.Logger().Info("Started EndpointWriter. connecting", slog.String("address", state.address)) 41 42 var err error 43 44 for i := 0; i < state.remote.config.MaxRetryCount; i++ { 45 err = state.initializeInternal() 46 if err != nil { 47 state.remote.Logger().Error("EndpointWriter failed to connect", slog.String("address", state.address), slog.Any("error", err), slog.Int("retry", i)) 48 // Wait 2 seconds to restart and retry 49 // Replace with Exponential Backoff 50 time.Sleep(2 * time.Second) 51 continue 52 } 53 54 break 55 } 56 57 if err != nil { 58 terminated := &EndpointTerminatedEvent{ 59 Address: state.address, 60 } 61 state.remote.actorSystem.EventStream.Publish(terminated) 62 63 return 64 65 // plog.Error("EndpointWriter failed to connect", log.String("address", state.address), log.Error(err)) 66 67 // Wait 2 seconds to restart and retry 68 // TODO: Replace with Exponential Backoff 69 // send this as a message to self - do not block the mailbox processing 70 // if in the meantime the actor is stopped (EndpointTerminated event), the message will be ignored (deadlettered) 71 // TODO: would it be a better idea to just publish EndpointTerminatedEvent here? to use the same path as when the connection is lost? 72 // time.AfterFunc(2*time.Second, func() { 73 // ctx.Send(ctx.Self(), &restartAfterConnectFailure{err}) 74 // }) 75 76 } 77 78 state.remote.Logger().Info("EndpointWriter connected", slog.String("address", state.address), slog.Duration("cost", time.Since(now))) 79 } 80 81 func (state *endpointWriter) initializeInternal() error { 82 conn, err := grpc.Dial(state.address, state.config.DialOptions...) 83 if err != nil { 84 return err 85 } 86 state.conn = conn 87 c := NewRemotingClient(conn) 88 stream, err := c.Receive(context.Background(), state.config.CallOptions...) 89 if err != nil { 90 state.remote.Logger().Error("EndpointWriter failed to create receive stream", slog.String("address", state.address), slog.Any("error", err)) 91 return err 92 } 93 state.stream = stream 94 95 err = stream.Send(&RemoteMessage{ 96 MessageType: &RemoteMessage_ConnectRequest{ 97 ConnectRequest: &ConnectRequest{ 98 ConnectionType: &ConnectRequest_ServerConnection{ 99 ServerConnection: &ServerConnection{ 100 SystemId: state.remote.actorSystem.ID, 101 Address: state.remote.actorSystem.Address(), 102 }, 103 }, 104 }, 105 }, 106 }) 107 if err != nil { 108 state.remote.Logger().Error("EndpointWriter failed to send connect request", slog.String("address", state.address), slog.Any("error", err)) 109 return err 110 } 111 112 connection, err := stream.Recv() 113 if err != nil { 114 state.remote.Logger().Error("EndpointWriter failed to receive connect response", slog.String("address", state.address), slog.Any("error", err)) 115 return err 116 } 117 118 switch connection.MessageType.(type) { 119 case *RemoteMessage_ConnectResponse: 120 state.remote.Logger().Debug("Received connect response", slog.String("fromAddress", state.address)) 121 // TODO: handle blocked status received from remote server 122 break 123 default: 124 state.remote.Logger().Error("EndpointWriter got invalid connect response", slog.String("address", state.address), slog.Any("type", connection.MessageType)) 125 return errors.New("invalid connect response") 126 } 127 128 go func() { 129 for { 130 _, err := stream.Recv() 131 switch { 132 case errors.Is(err, io.EOF): 133 state.remote.Logger().Debug("EndpointWriter stream completed", slog.String("address", state.address)) 134 return 135 case err != nil: 136 state.remote.Logger().Error("EndpointWriter lost connection", slog.String("address", state.address), slog.Any("error", err)) 137 terminated := &EndpointTerminatedEvent{ 138 Address: state.address, 139 } 140 state.remote.actorSystem.EventStream.Publish(terminated) 141 return 142 default: // DisconnectRequest 143 state.remote.Logger().Info("EndpointWriter got DisconnectRequest form remote", slog.String("address", state.address)) 144 terminated := &EndpointTerminatedEvent{ 145 Address: state.address, 146 } 147 state.remote.actorSystem.EventStream.Publish(terminated) 148 } 149 } 150 }() 151 152 connected := &EndpointConnectedEvent{Address: state.address} 153 state.remote.actorSystem.EventStream.Publish(connected) 154 return nil 155 } 156 157 func (state *endpointWriter) sendEnvelopes(msg []interface{}, ctx actor.Context) { 158 envelopes := make([]*MessageEnvelope, 0) 159 160 // type name uniqueness map name string to type index 161 typeNames := make(map[string]int32) 162 typeNamesArr := make([]string, 0) 163 164 targetNames := make(map[string]int32) 165 targetNamesArr := make([]*actor.PID, 0) 166 167 senderNames := make(map[string]int32) 168 senderNamesArr := make([]*actor.PID, 0) 169 170 var ( 171 header *MessageHeader 172 typeID int32 173 targetID int32 174 senderID int32 175 serializerID int32 176 ) 177 178 for _, tmp := range msg { 179 switch unwrapped := tmp.(type) { 180 case *EndpointTerminatedEvent, EndpointTerminatedEvent: 181 state.remote.Logger().Debug("Handling array wrapped terminate event", slog.String("address", state.address), slog.Any("message", unwrapped)) 182 ctx.Stop(ctx.Self()) 183 return 184 } 185 186 rd, _ := tmp.(*remoteDeliver) 187 188 if state.stream == nil { // not connected yet since first connection attempt failed and we are waiting for the retry 189 if rd.sender != nil { 190 state.remote.actorSystem.Root.Send(rd.sender, &actor.DeadLetterResponse{Target: rd.target}) 191 } else { 192 state.remote.actorSystem.EventStream.Publish(&actor.DeadLetterEvent{Message: rd.message, Sender: rd.sender, PID: rd.target}) 193 } 194 continue 195 } 196 197 if rd.header == nil || rd.header.Length() == 0 { 198 header = nil 199 } else { 200 header = &MessageHeader{ 201 HeaderData: rd.header.ToMap(), 202 } 203 } 204 205 // if the message can be translated to a serialization representation, we do this here 206 // this only apply to root level messages and never to nested child objects inside the message 207 message := rd.message 208 var err error 209 if v, ok := message.(RootSerializable); ok { 210 message, err = v.Serialize() 211 if err != nil { 212 state.remote.Logger().Error("EndpointWriter failed to serialize message", slog.String("address", state.address), slog.Any("error", err), slog.Any("message", v)) 213 continue 214 } 215 } 216 217 bytes, typeName, err := Serialize(message, serializerID) 218 if err != nil { 219 state.remote.Logger().Error("EndpointWriter failed to serialize message", slog.String("address", state.address), slog.Any("error", err), slog.Any("message", message)) 220 continue 221 } 222 typeID, typeNamesArr = addToLookup(typeNames, typeName, typeNamesArr) 223 targetID, targetNamesArr = addToTargetLookup(targetNames, rd.target, targetNamesArr) 224 targetRequestID := rd.target.RequestId 225 226 senderID, senderNamesArr = addToSenderLookup(senderNames, rd.sender, senderNamesArr) 227 senderRequestID := uint32(0) 228 if rd.sender != nil { 229 senderRequestID = rd.sender.RequestId 230 } 231 232 envelopes = append(envelopes, &MessageEnvelope{ 233 MessageHeader: header, 234 MessageData: bytes, 235 Sender: senderID, 236 Target: targetID, 237 TypeId: typeID, 238 SerializerId: serializerID, 239 TargetRequestId: targetRequestID, 240 SenderRequestId: senderRequestID, 241 }) 242 } 243 244 if len(envelopes) == 0 { 245 return 246 } 247 248 err := state.stream.Send(&RemoteMessage{ 249 MessageType: &RemoteMessage_MessageBatch{ 250 MessageBatch: &MessageBatch{ 251 TypeNames: typeNamesArr, 252 Targets: targetNamesArr, 253 Senders: senderNamesArr, 254 Envelopes: envelopes, 255 }, 256 }, 257 }) 258 if err != nil { 259 ctx.Stash() 260 state.remote.Logger().Debug("gRPC Failed to send", slog.String("address", state.address), slog.Any("error", err)) 261 ctx.Stop(ctx.Self()) 262 } 263 } 264 265 func addToLookup(m map[string]int32, name string, a []string) (int32, []string) { 266 max := int32(len(m)) 267 id, ok := m[name] 268 if !ok { 269 m[name] = max 270 id = max 271 a = append(a, name) 272 } 273 return id, a 274 } 275 276 func addToTargetLookup(m map[string]int32, pid *actor.PID, arr []*actor.PID) (int32, []*actor.PID) { 277 max := int32(len(m)) 278 key := pid.Address + "/" + pid.Id 279 id, ok := m[key] 280 if !ok { 281 c, _ := proto.Clone(pid).(*actor.PID) 282 c.RequestId = 0 283 m[key] = max 284 id = max 285 arr = append(arr, c) 286 } 287 return id, arr 288 } 289 290 func addToSenderLookup(m map[string]int32, pid *actor.PID, arr []*actor.PID) (int32, []*actor.PID) { 291 if pid == nil { 292 return 0, arr 293 } 294 295 max := int32(len(m)) 296 key := pid.Address + "/" + pid.Id 297 id, ok := m[key] 298 if !ok { 299 c, _ := proto.Clone(pid).(*actor.PID) 300 c.RequestId = 0 301 m[key] = max 302 id = max 303 arr = append(arr, c) 304 } 305 return id + 1, arr 306 } 307 308 func (state *endpointWriter) Receive(ctx actor.Context) { 309 switch msg := ctx.Message().(type) { 310 case *actor.Started: 311 state.initialize(ctx) 312 case *actor.Stopped: 313 state.remote.Logger().Debug("EndpointWriter stopped", slog.String("address", state.address)) 314 state.closeClientConn() 315 case *actor.Restarting: 316 state.remote.Logger().Debug("EndpointWriter restarting", slog.String("address", state.address)) 317 state.closeClientConn() 318 case *EndpointTerminatedEvent: 319 state.remote.Logger().Info("EndpointWriter received EndpointTerminatedEvent, stopping", slog.String("address", state.address)) 320 ctx.Stop(ctx.Self()) 321 case *restartAfterConnectFailure: 322 state.remote.Logger().Debug("EndpointWriter initiating self-restart after failing to connect and a delay", slog.String("address", state.address)) 323 panic(msg.err) 324 case []interface{}: 325 state.sendEnvelopes(msg, ctx) 326 case actor.SystemMessage, actor.AutoReceiveMessage: 327 // ignore 328 default: 329 state.remote.Logger().Error("EndpointWriter received unknown message", slog.String("address", state.address), slog.Any("message", msg)) 330 } 331 } 332 333 func (state *endpointWriter) closeClientConn() { 334 state.remote.Logger().Info("EndpointWriter closing client connection", slog.String("address", state.address)) 335 if state.stream != nil { 336 err := state.stream.CloseSend() 337 if err != nil { 338 state.remote.Logger().Error("EndpointWriter error when closing the stream", slog.Any("error", err)) 339 } 340 state.stream = nil 341 } 342 if state.conn != nil { 343 err := state.conn.Close() 344 if err != nil { 345 state.remote.Logger().Error("EndpointWriter error when closing the client conn", slog.Any("error", err)) 346 } 347 state.conn = nil 348 } 349 }