github.com/lirm/aeron-go@v0.0.0-20230415210743-920325491dc4/cluster/client/aeron_cluster.go (about) 1 // Copyright 2022 Steven Stern 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package client 16 17 import ( 18 "bytes" 19 "errors" 20 "fmt" 21 "strconv" 22 "strings" 23 "time" 24 25 "github.com/lirm/aeron-go/aeron" 26 "github.com/lirm/aeron-go/aeron/atomic" 27 "github.com/lirm/aeron-go/aeron/logbuffer" 28 "github.com/lirm/aeron-go/aeron/logging" 29 "github.com/lirm/aeron-go/aeron/util" 30 "github.com/lirm/aeron-go/cluster" 31 "github.com/lirm/aeron-go/cluster/codecs" 32 ) 33 34 var logger = logging.MustGetLogger("cluster-client") 35 var marshaller = codecs.NewSbeGoMarshaller() 36 37 var TemporaryError = errors.New("temporary error") 38 39 type AeronCluster struct { 40 opts *Options 41 aeronClient *aeron.Aeron 42 egressSub *aeron.Subscription 43 ingressChannel *aeron.ChannelUri 44 ingressPub *aeron.Publication 45 clusterSessionId int64 46 leadershipTermId int64 47 leaderMemberId int32 48 memberByIdMap map[int32]*memberIngress 49 fragmentAssembler *aeron.FragmentAssembler 50 egressListener EgressListener 51 sessionMsgHdrBuffer *atomic.Buffer 52 keepAliveBuffer *atomic.Buffer 53 state clientState 54 correlationId int64 55 nextRetryConnectTime int64 56 awaitTimeoutTime int64 57 } 58 59 type memberIngress struct { 60 memberId int32 61 endpoint string 62 publication *aeron.Publication 63 } 64 65 type clientState int8 66 67 const ( 68 clientDisconnected clientState = iota 69 clientCreatePublications 70 clientAwaitPublicationConnected 71 clientAwaitConnectReply 72 clientConnected 73 clientClosed 74 ) 75 76 const ( 77 protocolMajorVersion = 0 78 protocolMinorVersion = 2 79 protocolPatchVersion = 0 80 ) 81 82 var protocolSemanticVersion = util.SemanticVersionCompose( 83 protocolMajorVersion, protocolMinorVersion, protocolPatchVersion) 84 85 func NewAeronCluster( 86 aeronCtx *aeron.Context, 87 options *Options, 88 egressListener EgressListener, 89 ) (*AeronCluster, error) { 90 if egressListener == nil { 91 return nil, fmt.Errorf("egressListener is nil") 92 } 93 ingressChannel, err := aeron.ParseChannelUri(options.IngressChannel) 94 if err != nil { 95 return nil, err 96 } 97 if ingressChannel.IsIpc() && options.IngressEndpoints != "" { 98 return nil, fmt.Errorf("IngressEndpoints must be empty when using IPC ingress") 99 } 100 101 aeronClient, err := aeron.Connect(aeronCtx) 102 if err != nil { 103 return nil, err 104 } 105 106 egressSub, err := aeronClient.AddSubscription(options.EgressChannel, options.EgressStreamId) 107 if err != nil { 108 return nil, err 109 } 110 111 sessionMsgHdrBuf := codecs.MakeClusterMessageBuffer(cluster.SessionMessageHeaderTemplateId, cluster.SessionMessageHdrBlockLength) 112 113 client := &AeronCluster{ 114 opts: options, 115 aeronClient: aeronClient, 116 egressSub: egressSub, 117 ingressChannel: &ingressChannel, 118 clusterSessionId: cluster.NullValue, 119 leadershipTermId: cluster.NullValue, 120 leaderMemberId: cluster.NullValue, 121 memberByIdMap: make(map[int32]*memberIngress), 122 egressListener: egressListener, 123 state: clientDisconnected, 124 sessionMsgHdrBuffer: sessionMsgHdrBuf, 125 keepAliveBuffer: codecs.MakeClusterMessageBuffer(cluster.SessionKeepAliveTemplateId, 16), 126 } 127 client.fragmentAssembler = aeron.NewFragmentAssembler(client.onFragment, 0) 128 client.updateMemberEndpoints(options.IngressEndpoints) 129 130 return client, nil 131 } 132 133 func (ac *AeronCluster) ClusterSessionId() int64 { 134 return ac.clusterSessionId 135 } 136 137 func (ac *AeronCluster) LeadershipTermId() int64 { 138 return ac.leadershipTermId 139 } 140 141 func (ac *AeronCluster) LeaderMemberId() int32 { 142 return ac.leaderMemberId 143 } 144 145 func (ac *AeronCluster) IsConnected() bool { 146 return ac.state == clientConnected 147 } 148 149 func (ac *AeronCluster) IsClosed() bool { 150 return ac.state == clientClosed 151 } 152 153 func (ac *AeronCluster) Poll() int { 154 switch ac.state { 155 case clientDisconnected: 156 if time.Now().UnixMilli() > ac.nextRetryConnectTime { 157 ac.state = clientCreatePublications 158 } 159 case clientCreatePublications: 160 ret, err := ac.createPublications() 161 if err != nil { 162 logger.Warningf("error from createPublications %w", err) 163 } 164 return ret 165 case clientAwaitPublicationConnected: 166 ret, err := ac.awaitPublicationConnected() 167 if err != nil { 168 logger.Warningf("error from awaitPublicationConnected %w", err) 169 } 170 return ret 171 case clientAwaitConnectReply: 172 now := time.Now().UnixMilli() 173 if ac.ingressPub.IsConnected() && now < ac.awaitTimeoutTime { 174 return ac.pollEgress(1) 175 } else { 176 logger.Warningf("timed out waiting for session connect reply") 177 ac.state = clientDisconnected 178 ac.nextRetryConnectTime = now + (30 * time.Second).Milliseconds() 179 } 180 case clientConnected: 181 if ac.ingressPub.IsConnected() { 182 return ac.pollEgress(10) 183 // TODO: check if state == closed 184 } else { 185 ac.egressListener.OnDisconnect(ac, "ingress publication disconnected") 186 ac.state = clientCreatePublications 187 } 188 } 189 return 0 190 } 191 192 func (ac *AeronCluster) Offer(buffer *atomic.Buffer, offset, length int32) int64 { 193 if ac.state != clientConnected { 194 return aeron.NotConnected 195 } else { 196 hdrBuf := ac.sessionMsgHdrBuffer 197 return ac.ingressPub.Offer2(hdrBuf, 0, hdrBuf.Capacity(), buffer, offset, length, nil) 198 } 199 } 200 201 func (ac *AeronCluster) SendKeepAlive() bool { 202 if !ac.IsConnected() { 203 return false 204 } 205 buf := ac.keepAliveBuffer 206 for i := 0; i < 3; i++ { 207 if result := ac.ingressPub.Offer(buf, 0, buf.Capacity(), nil); result >= 0 { 208 return true 209 } 210 ac.opts.IdleStrategy.Idle(0) 211 } 212 return false 213 } 214 215 func (ac *AeronCluster) Close() { 216 if ac.IsConnected() && ac.ingressPub.IsConnected() { 217 ac.sendCloseSession() 218 } 219 if ac.ingressPub != nil { 220 if err := ac.ingressPub.Close(); err != nil { 221 logger.Debugf("error closing ingress publication: %v", err) 222 } 223 ac.ingressPub = nil 224 } 225 if ac.egressSub != nil { 226 if err := ac.egressSub.Close(); err != nil { 227 logger.Debugf("error closing egress subscription: %v", err) 228 } 229 ac.egressSub = nil 230 } 231 if err := ac.aeronClient.Close(); err != nil { 232 logger.Debugf("error closing aeron client: %v", err) 233 } 234 ac.state = clientClosed 235 } 236 237 func (ac *AeronCluster) updateMemberEndpoints(endpoints string) { 238 if endpoints == "" { 239 return 240 } 241 logger.Debugf("updateMemberEndpoints: %s", endpoints) 242 for idx, endpoint := range strings.Split(endpoints, ",") { 243 if delim := strings.IndexByte(endpoint, '='); delim > 0 { 244 memberId, err := strconv.Atoi(endpoint[:delim]) 245 if err != nil { 246 logger.Warningf("invalid endpoint at idx=%d: %s", idx, endpoint) 247 continue 248 } 249 address := endpoint[delim+1:] 250 member := ac.memberByIdMap[int32(memberId)] 251 if member == nil { 252 member = &memberIngress{ 253 memberId: int32(memberId), 254 endpoint: address, 255 } 256 ac.memberByIdMap[member.memberId] = member 257 } else if address != member.endpoint { 258 member.endpoint = address 259 if member.publication != nil { 260 member.close() 261 } 262 } 263 if member.memberId == ac.leaderMemberId { 264 if member.publication == nil { 265 pub, err := ac.createIngressPublication(address) 266 if err == nil { 267 member.publication = pub 268 } else { 269 logger.Warning(err) 270 } 271 } 272 ac.ingressPub = member.publication 273 ac.fragmentAssembler.Clear() 274 } 275 } else { 276 logger.Warningf("endpoint at idx=%d missing '=' separator: %s", idx, endpoint) 277 } 278 } 279 } 280 281 func (ac *AeronCluster) createPublications() (int, error) { 282 if len(ac.memberByIdMap) > 0 { 283 for _, member := range ac.memberByIdMap { 284 if member.publication == nil { 285 pub, err := ac.createIngressPublication(member.endpoint) 286 if err != nil { 287 return 0, err 288 } 289 member.publication = pub 290 } 291 } 292 } else if ac.ingressPub == nil { 293 pub, err := ac.createIngressPublication(ac.opts.IngressChannel) 294 if err != nil { 295 return 0, err 296 } 297 ac.ingressPub = pub 298 } 299 ac.state = clientAwaitPublicationConnected 300 ac.awaitTimeoutTime = time.Now().UnixMilli() + (5 * time.Second).Milliseconds() 301 return 1, nil 302 } 303 304 func (ac *AeronCluster) createIngressPublication(endpoint string) (*aeron.Publication, error) { 305 if ac.ingressChannel.IsUdp() { 306 ac.ingressChannel.Set("endpoint", endpoint) 307 } 308 channel := ac.ingressChannel.String() 309 logger.Debugf("createIngressPublication - endpoint=%s isUdp=%v isExclusive=%v", 310 endpoint, ac.ingressChannel.IsUdp(), ac.opts.IsIngressExclusive) 311 if ac.opts.IsIngressExclusive { 312 return ac.aeronClient.AddExclusivePublication(channel, ac.opts.IngressStreamId) 313 } else { 314 return ac.aeronClient.AddPublication(channel, ac.opts.IngressStreamId) 315 } 316 } 317 318 func (ac *AeronCluster) awaitPublicationConnected() (int, error) { 319 responseChannel := ac.egressSub.TryResolveChannelEndpointPort() 320 if responseChannel == "" { 321 // TODO: Is this an error or success condition? 322 return 0, nil 323 } 324 now := time.Now().UnixMilli() 325 if now > ac.awaitTimeoutTime { 326 ac.state = clientDisconnected 327 // close publications? shouldn't be necessary unless we've hit some bug 328 ac.nextRetryConnectTime = now + (30 * time.Second).Milliseconds() 329 return 0, errors.New("timed out waiting for connected publication") 330 } 331 if len(ac.memberByIdMap) > 0 { 332 for _, member := range ac.memberByIdMap { 333 if member.publication != nil && member.publication.IsConnected() { 334 ac.ingressPub = member.publication 335 ac.fragmentAssembler.Clear() 336 err := ac.sendConnectRequest(responseChannel) 337 if err == nil { 338 logger.Debugf("sent connect request to memberId=%d correlationId=%d channel=%s", 339 member.memberId, ac.correlationId, member.publication.Channel()) 340 ac.state = clientAwaitConnectReply 341 ac.awaitTimeoutTime = now + (3 * time.Second).Milliseconds() 342 break 343 } 344 if !errors.Is(err, TemporaryError) { 345 return 0, err 346 } 347 } 348 } 349 } else if ac.ingressPub.IsConnected() && ac.sendConnectRequest(responseChannel) == nil { 350 ac.state = clientAwaitConnectReply 351 ac.awaitTimeoutTime = now + (3 * time.Second).Milliseconds() 352 } 353 return 0, nil 354 } 355 356 // Returns nil on success, TemporaryError, or any other error is a permanent error. 357 func (ac *AeronCluster) sendConnectRequest(responseChannel string) error { 358 ac.correlationId = ac.aeronClient.NextCorrelationID() 359 req := codecs.SessionConnectRequest{ 360 CorrelationId: ac.correlationId, 361 ResponseStreamId: ac.opts.EgressStreamId, 362 Version: int32(protocolSemanticVersion), 363 ResponseChannel: []byte(responseChannel), 364 } 365 header := codecs.MessageHeader{ 366 BlockLength: req.SbeBlockLength(), 367 TemplateId: req.SbeTemplateId(), 368 SchemaId: req.SbeSchemaId(), 369 Version: req.SbeSchemaVersion(), 370 } 371 writer := new(bytes.Buffer) 372 if err := header.Encode(marshaller, writer); err != nil { 373 return err 374 } 375 if err := req.Encode(marshaller, writer, true); err != nil { 376 return err 377 } 378 buffer := atomic.MakeBuffer(writer.Bytes()) 379 result := ac.ingressPub.Offer(buffer, 0, buffer.Capacity(), nil) 380 if result >= 0 { 381 return nil 382 } else { 383 return fmt.Errorf("%w, failed to send connect request, channel=%s result=%d", 384 TemporaryError, ac.ingressPub.Channel(), result) 385 } 386 } 387 388 func (ac *AeronCluster) pollEgress(fragmentLimit int) int { 389 return ac.egressSub.Poll(ac.fragmentAssembler.OnFragment, fragmentLimit) 390 } 391 392 func (ac *AeronCluster) onFragment(buffer *atomic.Buffer, offset, length int32, header *logbuffer.Header) { 393 if length < cluster.SBEHeaderLength { 394 return 395 } 396 blockLength := buffer.GetUInt16(offset) 397 templateId := buffer.GetUInt16(offset + 2) 398 schemaId := buffer.GetUInt16(offset + 4) 399 version := buffer.GetUInt16(offset + 6) 400 if schemaId != cluster.ClusterSchemaId { 401 logger.Errorf("unexpected schemaId=%d templateId=%d blockLen=%d version=%d", 402 schemaId, templateId, blockLength, version) 403 return 404 } 405 offset += cluster.SBEHeaderLength 406 length -= cluster.SBEHeaderLength 407 408 switch templateId { 409 case cluster.SessionMessageHeaderTemplateId: 410 ac.onSessionMessage(buffer, offset, length, header) 411 case cluster.SessionEventTemplateId: 412 ac.onSessionEvent(buffer, offset, length, version, blockLength) 413 case cluster.NewLeaderEventTemlateId: 414 ac.onNewLeaderEvent(buffer, offset, length, version, blockLength) 415 case cluster.ChallengeTemplateId: 416 e := codecs.Challenge{} 417 buf := bytes.Buffer{} 418 buffer.WriteBytes(&buf, offset, length) 419 if err := e.Decode(marshaller, &buf, version, blockLength, true); err != nil { 420 logger.Errorf("new leader event decode error: %v", err) 421 } else { 422 logger.Warningf("received challenge, corrId=%d clusterSessionId=%d", e.CorrelationId, e.ClusterSessionId) 423 } 424 } 425 } 426 427 func (ac *AeronCluster) onNewLeaderEvent(buffer *atomic.Buffer, offset, length int32, version, blockLength uint16) { 428 e := codecs.NewLeaderEvent{} 429 buf := bytes.Buffer{} 430 buffer.WriteBytes(&buf, offset, length) 431 if err := e.Decode(marshaller, &buf, version, blockLength, true); err != nil { 432 logger.Errorf("new leader event decode error: %v", err) 433 } else if ac.state == clientConnected && e.ClusterSessionId == ac.clusterSessionId { 434 ac.leadershipTermId = e.LeadershipTermId 435 ac.leaderMemberId = e.LeaderMemberId 436 ac.sessionMsgHdrBuffer.PutInt64(cluster.SBEHeaderLength, e.LeadershipTermId) 437 ac.keepAliveBuffer.PutInt64(cluster.SBEHeaderLength, e.LeadershipTermId) 438 if ac.opts.IngressEndpoints != "" { 439 if err := ac.ingressPub.Close(); err != nil { 440 logger.Warningf("error closing ingress publication: %v", err) 441 } 442 ac.ingressPub = nil 443 ac.updateMemberEndpoints(string(e.IngressEndpoints)) 444 } 445 ac.fragmentAssembler.Clear() 446 ac.egressListener.OnNewLeader(ac, e.LeadershipTermId, e.LeaderMemberId) 447 } else { 448 logger.Debugf("ignored new leader event - state=%v thisSessionId=%d targetSessionId=%d leaderMemberId=%d leaderTermId=%d", 449 ac.state, ac.clusterSessionId, e.ClusterSessionId, e.LeaderMemberId, e.LeadershipTermId) 450 } 451 } 452 453 func (ac *AeronCluster) onSessionEvent(buffer *atomic.Buffer, offset, length int32, version, blockLength uint16) { 454 e := codecs.SessionEvent{} 455 buf := bytes.Buffer{} 456 buffer.WriteBytes(&buf, offset, length) 457 if err := e.Decode(marshaller, &buf, version, blockLength, true); err != nil { 458 logger.Errorf("session event decode error: %v", err) 459 } else if ac.state == clientAwaitConnectReply && e.CorrelationId == ac.correlationId { 460 switch e.Code { 461 case codecs.EventCode.OK: 462 ac.leadershipTermId = e.LeadershipTermId 463 ac.leaderMemberId = e.LeaderMemberId 464 ac.clusterSessionId = e.ClusterSessionId 465 ac.sessionMsgHdrBuffer.PutInt64(cluster.SBEHeaderLength, e.LeadershipTermId) 466 ac.sessionMsgHdrBuffer.PutInt64(cluster.SBEHeaderLength+8, e.ClusterSessionId) 467 ac.keepAliveBuffer.PutInt64(cluster.SBEHeaderLength, e.LeadershipTermId) 468 ac.keepAliveBuffer.PutInt64(cluster.SBEHeaderLength+8, e.ClusterSessionId) 469 ac.state = clientConnected 470 ac.closeNonLeaderPublications() 471 ac.egressListener.OnConnect(ac) 472 case codecs.EventCode.REDIRECT: 473 logger.Infof("got redirect - leaderTermId=%d leaderMemberId=%d", e.LeadershipTermId, e.LeaderMemberId) 474 ac.leaderMemberId = e.LeaderMemberId 475 ac.updateMemberEndpoints(string(e.Detail)) 476 ac.closeNonLeaderPublications() 477 ac.state = clientAwaitPublicationConnected 478 case codecs.EventCode.ERROR: 479 ac.egressListener.OnError(ac, string(e.Detail)) 480 ac.state = clientDisconnected 481 ac.nextRetryConnectTime = time.Now().UnixMilli() + (5 * time.Second).Milliseconds() 482 case codecs.EventCode.AUTHENTICATION_REJECTED: 483 ac.egressListener.OnError(ac, fmt.Sprintf("authentication rejected (%s)", string(e.Detail))) 484 ac.state = clientDisconnected 485 ac.nextRetryConnectTime = time.Now().UnixMilli() + time.Minute.Milliseconds() 486 } 487 } else if ac.state == clientConnected && e.ClusterSessionId == ac.clusterSessionId { 488 if e.Code == codecs.EventCode.CLOSED { 489 ac.egressListener.OnDisconnect(ac, string(e.Detail)) 490 ac.state = clientClosed 491 } else if e.Code == codecs.EventCode.ERROR { 492 ac.egressListener.OnError(ac, string(e.Detail)) 493 } else { 494 logger.Infof("onSessionEvent - code=%v (%s)", e.Code, string(e.Detail)) 495 } 496 } else { 497 logger.Debugf("ignored session event - state=%v thisSessionId=%d targetSessionId=%d code=%d (%s)", 498 ac.state, ac.clusterSessionId, e.ClusterSessionId, e.Code, string(e.Detail)) 499 } 500 } 501 502 func (ac *AeronCluster) closeNonLeaderPublications() { 503 for _, member := range ac.memberByIdMap { 504 if member.memberId != ac.leaderMemberId { 505 member.close() 506 } 507 } 508 } 509 510 func (ac *AeronCluster) onSessionMessage(buffer *atomic.Buffer, offset, length int32, header *logbuffer.Header) { 511 if length < cluster.SessionMessageHeaderLength { 512 logger.Errorf("received invalid session message - length: %d", length) 513 return 514 } 515 leadershipTermId := buffer.GetInt64(offset) 516 clusterSessionId := buffer.GetInt64(offset + 8) 517 timestamp := buffer.GetInt64(offset + 16) 518 if ac.state != clientConnected { 519 logger.Debugf("received unexpected session message - leadershipTermId=%d clusterSessionId=%d state=%v", 520 leadershipTermId, clusterSessionId, ac.state) 521 } else if clusterSessionId == ac.clusterSessionId { 522 ac.egressListener.OnMessage(ac, timestamp, buffer, offset+cluster.SessionMessageHeaderLength, 523 length-cluster.SessionMessageHeaderLength, header) 524 } else { 525 logger.Debugf("received unexpected session msg - leaderTermId=%d targetSessionId=%d thisSessionId=%d", 526 leadershipTermId, clusterSessionId, ac.clusterSessionId) 527 } 528 } 529 530 func (ac *AeronCluster) sendCloseSession() { 531 buf := codecs.MakeClusterMessageBuffer(cluster.SessionCloseRequestTemplateId, 16) 532 buf.PutInt64(cluster.SBEHeaderLength, ac.leadershipTermId) 533 buf.PutInt64(cluster.SBEHeaderLength+8, ac.clusterSessionId) 534 for i := 0; i < 3; i++ { 535 if result := ac.ingressPub.Offer(buf, 0, buf.Capacity(), nil); result >= 0 { 536 return 537 } 538 ac.opts.IdleStrategy.Idle(0) 539 } 540 } 541 542 func (member *memberIngress) close() { 543 if member.publication != nil { 544 if err := member.publication.Close(); err != nil { 545 logger.Warningf("error closing member publication, memberId=%d endpoint=%s: %v", 546 member.memberId, member.endpoint, err) 547 } 548 member.publication = nil 549 } 550 }