-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
added visibility_timeout.go #11
Changes from 4 commits
2977e16
eec4f66
871e019
3644a6f
2c14324
ee9713e
b7bb7f1
f490a08
bc1190c
1523810
37c89b0
ba02771
8ffd918
39f1944
b7f0537
0450426
c3382b7
2020f23
d3a4f01
1821064
14d86b1
2e8255e
33129d2
03a4f94
90fd323
c1151e2
815bf95
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,12 +17,42 @@ type server struct { | |
client *spanner.Client | ||
op *hedge.Op | ||
pb.UnimplementedPubSubServiceServer | ||
|
||
visibilityTimeouts sync.Map // messageID -> VisibilityInfo | ||
lockMu sync.RWMutex | ||
|
||
messageQueue map[string][]*pb.Message // topic -> messages | ||
messageQueueMu sync.RWMutex | ||
} | ||
|
||
type broadCastInput struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to use the one in broadcast.go? |
||
Type string `json:"type"` | ||
Msg interface{} `json:"msg"` | ||
} | ||
|
||
type VisibilityInfo struct { | ||
MessageID string `json:"messageId"` | ||
SubscriberID string `json:"subscriberId"` | ||
ExpiresAt time.Time `json:"expiresAt"` | ||
NodeID string `json:"nodeId"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. May I now what is NodeID for? It seems it's an uuid and generated every locking of message. So I assume it's different per message? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NodeID is generated as a UUID when locking a message. it is different per message, it means each message lock instance gets a unique identifier. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see, when can nodeID be used? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it can be useful in distributed environments where multiple nodes manage message processing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should i remove it sir? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's discuss this further. |
||
} | ||
|
||
const ( | ||
MessagesTable = "Messages" | ||
visibilityTimeout = 5 * time.Minute | ||
cleanupInterval = 30 * time.Second | ||
) | ||
|
||
func NewServer(client *spanner.Client, op *hedge.Op) *server { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to initialize server There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can try to do it in main.go |
||
s := &server{ | ||
client: client, | ||
op: op, | ||
messageQueue: make(map[string][]*pb.Message), | ||
} | ||
go s.startVisibilityCleanup() | ||
return s | ||
} | ||
|
||
func (s *server) Publish(ctx context.Context, in *pb.PublishRequest) (*pb.PublishResponse, error) { | ||
if in.Topic == "" { | ||
return nil, status.Error(codes.InvalidArgument, "topic must not be empty") | ||
|
@@ -38,16 +68,18 @@ func (s *server) Publish(ctx context.Context, in *pb.PublishRequest) (*pb.Publis | |
|
||
messageID := uuid.New().String() | ||
mutation := spanner.InsertOrUpdate( | ||
MessagesTable, | ||
[]string{"id", "topic", "payload", "createdAt", "updatedAt"}, | ||
[]interface{}{ | ||
messageID, | ||
in.Topic, | ||
in.Payload, | ||
spanner.CommitTimestamp, | ||
spanner.CommitTimestamp, | ||
}, | ||
) | ||
MessagesTable, | ||
[]string{"id", "topic", "payload", "createdAt", "updatedAt", "visibilityTimeout", "processed"}, | ||
[]interface{}{ | ||
messageID, | ||
in.Topic, | ||
in.Payload, | ||
spanner.CommitTimestamp, | ||
spanner.CommitTimestamp, | ||
nil, // Explicitly set visibilityTimeout as NULL | ||
false, // Default to unprocessed | ||
}, | ||
) | ||
|
||
_, err := s.client.Apply(ctx, []*spanner.Mutation{mutation}) | ||
if err != nil { | ||
|
@@ -72,3 +104,211 @@ func (s *server) Publish(ctx context.Context, in *pb.PublishRequest) (*pb.Publis | |
log.Printf("[Publish] Message successfully broadcasted and wrote to spanner with ID: %s", messageID) | ||
return &pb.PublishResponse{MessageId: messageID}, nil | ||
} | ||
|
||
func (s *server) Subscribe(req *pb.SubscribeRequest, stream pb.PubSubService_SubscribeServer) error { | ||
subscriberID := uuid.New().String() | ||
ctx := stream.Context() | ||
|
||
log.Printf("[Subscribe] New subscriber: %s for topic: %s", subscriberID, req.Topic) | ||
go s.keepAliveSubscriber(ctx, stream) | ||
|
||
for { | ||
select { | ||
case <-ctx.Done(): | ||
s.cleanupSubscriberLocks(subscriberID) | ||
return nil | ||
default: | ||
s.messageQueueMu.RLock() | ||
msgs, exists := s.messageQueue[req.Topic] | ||
s.messageQueueMu.RUnlock() | ||
|
||
if !exists || len(msgs) == 0 { | ||
time.Sleep(100 * time.Millisecond) | ||
continue | ||
} | ||
// Check visibility timeout before sending | ||
info, exists := s.visibilityTimeouts.Load(msg.Id) | ||
if exists && time.Now().Before(info.(VisibilityInfo).ExpiresAt) { | ||
continue // Skip locked messages | ||
} | ||
|
||
s.messageQueueMu.Lock() | ||
msg := msgs[0] | ||
s.messageQueue[req.Topic] = msgs[1:] | ||
s.messageQueueMu.Unlock() | ||
|
||
|
||
locked, err := s.tryLockMessage(msg.Id, subscriberID) | ||
if err != nil || !locked { | ||
continue | ||
} | ||
|
||
if err := stream.Send(msg); err != nil { | ||
s.releaseMessageLock(msg.Id, subscriberID) | ||
return err | ||
} | ||
} | ||
} | ||
} | ||
|
||
func (s *server) tryLockMessage(messageID, subscriberID string) (bool, error) { | ||
s.lockMu.Lock() | ||
defer s.lockMu.Unlock() | ||
|
||
if _, exists := s.visibilityTimeouts.Load(messageID); exists { | ||
return false, nil | ||
} | ||
|
||
visInfo := VisibilityInfo{ | ||
MessageID: messageID, | ||
SubscriberID: subscriberID, | ||
ExpiresAt: time.Now().Add(visibilityTimeout), | ||
NodeID: uuid.New().String(), | ||
} | ||
|
||
s.visibilityTimeouts.Store(messageID, visInfo) | ||
return true, s.broadcastVisibilityUpdate("lock", visInfo) | ||
} | ||
|
||
func (s *server) Acknowledge(ctx context.Context, req *pb.AcknowledgeRequest) (*pb.AcknowledgeResponse, error) { | ||
s.messageQueueMu.Lock() | ||
defer s.messageQueueMu.Unlock() | ||
|
||
if err := s.releaseMessageLock(req.Id, req.SubscriberId); err != nil { | ||
log.Printf("Error releasing message lock: %v", err) | ||
} | ||
|
||
mutation := spanner.Update( | ||
MessagesTable, | ||
[]string{"id", "processed", "updatedAt"}, | ||
[]interface{}{req.Id, true, spanner.CommitTimestamp}, | ||
) | ||
|
||
_, err := s.client.Apply(ctx, []*spanner.Mutation{mutation}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
s.messageQueue[req.Topic] = s.messageQueue[req.Topic][1:] | ||
|
||
bcastin := broadCastInput{ | ||
Type: "ack", | ||
Msg: map[string]string{ | ||
"messageId": req.Id, | ||
"topic": req.Topic, | ||
}, | ||
} | ||
if err := s.broadcastAck(bcastin); err != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems that there are no definitions for broadcastAck() |
||
log.Printf("Error broadcasting ack: %v", err) | ||
} | ||
|
||
return &pb.AcknowledgeResponse{Success: true}, nil | ||
} | ||
|
||
func (s *server) releaseMessageLock(messageID, subscriberID string) error { | ||
s.lockMu.Lock() | ||
defer s.lockMu.Unlock() | ||
|
||
if info, exists := s.visibilityTimeouts.Load(messageID); exists { | ||
visInfo := info.(VisibilityInfo) | ||
if visInfo.SubscriberID == subscriberID { | ||
s.visibilityTimeouts.Delete(messageID) | ||
return s.broadcastVisibilityUpdate("unlock", visInfo) | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (s *server) ExtendVisibilityTimeout(ctx context.Context, req *pb.ExtendTimeoutRequest) (*pb.ExtendTimeoutResponse, error) { | ||
s.lockMu.Lock() | ||
defer s.lockMu.Unlock() | ||
|
||
info, exists := s.visibilityTimeouts.Load(req.MessageId) | ||
if !exists { | ||
return nil, status.Error(codes.NotFound, "Message lock not found") | ||
} | ||
|
||
visInfo := info.(VisibilityInfo) | ||
if visInfo.SubscriberID != req.SubscriberId { | ||
return nil, status.Error(codes.PermissionDenied, "Not allowed to extend timeout for this message") | ||
} | ||
|
||
newExpiry := time.Now().Add(time.Duration(req.ExtensionSeconds) * time.Second) | ||
visInfo.ExpiresAt = newExpiry | ||
s.visibilityTimeouts.Store(req.MessageId, visInfo) | ||
|
||
// Update Spanner to reflect the new timeout | ||
go func() { | ||
mutation := spanner.Update( | ||
MessagesTable, | ||
[]string{"id", "visibilityTimeout", "updatedAt"}, | ||
[]interface{}{req.MessageId, newExpiry, spanner.CommitTimestamp}, | ||
) | ||
_, err := s.client.Apply(ctx, []*spanner.Mutation{mutation}) | ||
if err != nil { | ||
log.Printf("Spanner update error: %v", err) | ||
} | ||
}() | ||
|
||
// Broadcast new timeout info | ||
_ = s.broadcastVisibilityUpdate("extend", visInfo) | ||
|
||
return &pb.ExtendTimeoutResponse{Success: true}, nil | ||
} | ||
|
||
|
||
func (s *server) broadcastVisibilityUpdate(cmdType string, info VisibilityInfo) error { | ||
bcastin := broadCastInput{ | ||
Type: "visibility", | ||
Msg: struct { | ||
Command string `json:"command"` | ||
Info VisibilityInfo `json:"info"` | ||
}{ | ||
Command: cmdType, | ||
Info: info, | ||
}, | ||
} | ||
|
||
data, err := json.Marshal(bcastin) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
results := s.op.Broadcast(context.Background(), data) | ||
for _, result := range results { | ||
if result.Error != nil { | ||
log.Printf("Broadcast error to node %s: %v", result.NodeID, result.Error) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (s *server) startVisibilityCleanup() { | ||
ticker := time.NewTicker(cleanupInterval) | ||
for range ticker.C { | ||
s.cleanupExpiredLocks() | ||
} | ||
} | ||
|
||
func (s *server) cleanupExpiredLocks() { | ||
now := time.Now() | ||
s.lockMu.Lock() | ||
defer s.lockMu.Unlock() | ||
|
||
s.visibilityTimeouts.Range(func(key, value interface{}) bool { | ||
visInfo := value.(VisibilityInfo) | ||
if now.After(visInfo.ExpiresAt) { | ||
// Double-check before deleting | ||
if info, exists := s.visibilityTimeouts.Load(key); exists { | ||
if time.Now().Before(info.(VisibilityInfo).ExpiresAt) { | ||
return true // Another node extended it | ||
} | ||
} | ||
s.visibilityTimeouts.Delete(key) | ||
s.broadcastVisibilityUpdate("unlock", visInfo) | ||
} | ||
return true | ||
}) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move this to PubSub object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move it from the server struct into a separate PubSub struct. is this what you mean sir?
maybe like this?
type PubSub struct {
visibilityTimeouts sync.Map // messageID -> VisibilityInfo
lockMu sync.RWMutex
messageQueue map[string][]*pb.Message // topic -> messages
messageQueueMu sync.RWMutex
}
type server struct {
client *spanner.Client
op *hedge.Op
pb.UnimplementedPubSubServiceServer
pubSub *PubSub
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please check app.go