Re-send messages with ResendAutomatically=true

This commit is contained in:
Vitaly Vlasov 2023-11-23 00:43:22 +02:00 committed by Vit∀ly Vlasov
parent c433908834
commit 384543d3a6
10 changed files with 776 additions and 444 deletions

View File

@ -210,6 +210,7 @@ func newCommunitiesTestMessenger(shh types.Waku, privateKey *ecdsa.PrivateKey, l
WithMultiAccounts(madb),
WithAccount(iai.ToMultiAccount()),
WithDatasync(),
WithResendParams(3, 3),
WithTokenManager(tokenManager),
}

View File

@ -82,9 +82,6 @@ const (
privateChat chatContext = "private-chat"
)
const messageResendMinDelay = 30
const messageResendMaxCount = 3
var communityAdvertiseIntervalSecond int64 = 60 * 60
// messageCacheIntervalMs is how long we should keep processed messages in the cache, in ms
@ -287,7 +284,7 @@ func NewMessenger(
) (*Messenger, error) {
var messenger *Messenger
c := config{}
c := config{messageResendMinDelay: 30, messageResendMaxCount: 3}
for _, opt := range opts {
if err := opt(&c); err != nil {
@ -667,22 +664,9 @@ func (m *Messenger) processSentMessages(ids []string) error {
return nil
}
func shouldResendMessage(message *common.RawMessage, t common.TimeSource) (bool, error) {
if !(message.MessageType == protobuf.ApplicationMetadataMessage_EMOJI_REACTION ||
message.MessageType == protobuf.ApplicationMetadataMessage_CHAT_MESSAGE) {
return false, errors.Errorf("Should resend only specific types of messages, can't resend %v", message.MessageType)
}
if message.Sent {
return false, errors.New("Should resend only non-sent messages")
}
if message.SendCount > messageResendMaxCount {
return false, nil
}
func (m *Messenger) shouldResendMessage(message *common.RawMessage, t common.TimeSource) (bool, error) {
//exponential backoff depends on how many attempts to send message already made
backoff := uint64(math.Pow(2, float64(message.SendCount-1))) * messageResendMinDelay * uint64(time.Second.Milliseconds())
backoff := uint64(math.Pow(2, float64(message.SendCount-1))) * uint64(m.config.messageResendMinDelay) * uint64(time.Second.Milliseconds())
backoffElapsed := t.GetCurrentTime() > (message.LastSent + backoff)
return backoffElapsed, nil
}
@ -692,7 +676,7 @@ func (m *Messenger) resendExpiredMessages() error {
return errors.New("offline")
}
ids, err := m.persistence.ExpiredMessagesIDs(messageResendMaxCount)
ids, err := m.persistence.ExpiredMessagesIDs(m.config.messageResendMaxCount)
if err != nil {
return errors.Wrapf(err, "Can't get expired reactions from db")
}
@ -712,7 +696,7 @@ func (m *Messenger) resendExpiredMessages() error {
return errors.New("Only public chats and community chats messages are resent")
}
ok, err = shouldResendMessage(rawMessage, m.getTimesource())
ok, err = m.shouldResendMessage(rawMessage, m.getTimesource())
if err != nil {
return err
}

View File

@ -109,6 +109,9 @@ type config struct {
telemetryServerURL string
wakuService *wakuv2.Waku
messageResendMinDelay int
messageResendMaxCount int
}
type Option func(*config) error
@ -136,6 +139,14 @@ func WithVerifyTransactionClient(client EthClient) Option {
}
}
func WithResendParams(minDelay int, maxCount int) Option {
return func(c *config) error {
c.messageResendMinDelay = minDelay
c.messageResendMaxCount = maxCount
return nil
}
}
func WithDatabase(db *sql.DB) Option {
return func(c *config) error {
c.appDb = db

View File

@ -0,0 +1,190 @@
package protocol
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/suite"
"go.uber.org/zap"
gethbridge "github.com/status-im/status-go/eth-node/bridge/geth"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/communities"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/protocol/requests"
"github.com/status-im/status-go/protocol/tt"
)
type MessengerOfflineSuite struct {
suite.Suite
owner *Messenger
bob *Messenger
alice *Messenger
ownerWaku types.Waku
bobWaku types.Waku
aliceWaku types.Waku
logger *zap.Logger
}
func TestMessengerOfflineSuite(t *testing.T) {
suite.Run(t, new(MessengerOfflineSuite))
}
func (s *MessengerOfflineSuite) SetupTest() {
s.logger = tt.MustCreateTestLogger()
wakuNodes := CreateWakuV2Network(&s.Suite, s.logger, []string{"owner", "bob", "alice"})
ownerLogger := s.logger.With(zap.String("name", "owner"))
s.ownerWaku = wakuNodes[0]
s.owner = s.newMessenger(s.ownerWaku, ownerLogger)
bobLogger := s.logger.With(zap.String("name", "bob"))
s.bobWaku = wakuNodes[1]
s.bob = s.newMessenger(s.bobWaku, bobLogger)
aliceLogger := s.logger.With(zap.String("name", "alice"))
s.aliceWaku = wakuNodes[2]
s.alice = s.newMessenger(s.aliceWaku, aliceLogger)
_, err := s.owner.Start()
s.Require().NoError(err)
_, err = s.bob.Start()
s.Require().NoError(err)
_, err = s.alice.Start()
s.Require().NoError(err)
s.owner.communitiesManager.RekeyInterval = 50 * time.Millisecond
}
func (s *MessengerOfflineSuite) TearDownTest() {
if s.owner != nil {
s.Require().NoError(s.owner.Shutdown())
}
if s.ownerWaku != nil {
s.Require().NoError(gethbridge.GetGethWakuV2From(s.ownerWaku).Stop())
}
if s.bob != nil {
s.Require().NoError(s.bob.Shutdown())
}
if s.bobWaku != nil {
s.Require().NoError(gethbridge.GetGethWakuV2From(s.bobWaku).Stop())
}
if s.alice != nil {
s.Require().NoError(s.alice.Shutdown())
}
if s.aliceWaku != nil {
s.Require().NoError(gethbridge.GetGethWakuV2From(s.aliceWaku).Stop())
}
_ = s.logger.Sync()
}
func (s *MessengerOfflineSuite) newMessenger(waku types.Waku, logger *zap.Logger) *Messenger {
privateKey, err := crypto.GenerateKey()
s.Require().NoError(err)
m, err := newCommunitiesTestMessenger(waku, privateKey, logger, nil, nil, nil)
s.Require().NoError(err)
return m
}
func (s *MessengerOfflineSuite) advertiseCommunityTo(community *communities.Community, owner *Messenger, user *Messenger) {
advertiseCommunityTo(&s.Suite, community, owner, user)
}
func (s *MessengerOfflineSuite) joinCommunity(community *communities.Community, owner *Messenger, user *Messenger) {
request := &requests.RequestToJoinCommunity{CommunityID: community.ID()}
joinCommunity(&s.Suite, community, owner, user, request, "")
}
func (s *MessengerOfflineSuite) TestCommunityOfflineEdit() {
community, chat := createCommunity(&s.Suite, s.owner)
chatID := chat.ID
inputMessage := common.NewMessage()
inputMessage.ChatId = chatID
inputMessage.ContentType = protobuf.ChatMessage_TEXT_PLAIN
inputMessage.Text = "some text"
ctx := context.Background()
s.advertiseCommunityTo(community, s.owner, s.alice)
s.joinCommunity(community, s.owner, s.alice)
_, err := s.alice.SendChatMessage(ctx, inputMessage)
s.Require().NoError(err)
s.checkMessageDelivery(ctx, inputMessage)
// Simulate going offline
wakuv2 := gethbridge.GetGethWakuV2From(s.aliceWaku)
wakuv2.SkipPublishToTopic(true)
resp, err := s.alice.SendChatMessage(ctx, inputMessage)
messageID := types.Hex2Bytes(resp.Messages()[0].ID)
s.Require().NoError(err)
// Check that message is re-sent once back online
wakuv2.SkipPublishToTopic(false)
time.Sleep(5 * time.Second)
s.checkMessageDelivery(ctx, inputMessage)
editedText := "some text edited"
editedMessage := &requests.EditMessage{
ID: messageID,
Text: editedText,
}
wakuv2.SkipPublishToTopic(true)
sendResponse, err := s.alice.EditMessage(ctx, editedMessage)
s.Require().NotNil(sendResponse)
s.Require().NoError(err)
// Check that message is re-sent once back online
wakuv2.SkipPublishToTopic(false)
time.Sleep(5 * time.Second)
inputMessage.Text = editedText
s.checkMessageDelivery(ctx, inputMessage)
}
func (s *MessengerOfflineSuite) checkMessageDelivery(ctx context.Context, inputMessage *common.Message) {
var response *MessengerResponse
// Pull message and make sure org is received
err := tt.RetryWithBackOff(func() error {
var err error
response, err = s.owner.RetrieveAll()
if err != nil {
return err
}
if len(response.messages) == 0 {
return errors.New("message not received")
}
return nil
})
s.Require().NoError(err)
s.Require().Len(response.Messages(), 1)
s.Require().Equal(inputMessage.Text, response.Messages()[0].Text)
// check if response contains the chat we're interested in
// we use this instead of checking just the length of the chat because
// a CommunityDescription message might be received in the meantime due to syncing
// hence response.Chats() might contain the general chat, and the new chat;
// or only the new chat if the CommunityDescription message has not arrived
found := false
for _, chat := range response.Chats() {
if chat.ID == inputMessage.ChatId {
found = true
}
}
s.Require().True(found)
}

View File

@ -2220,74 +2220,76 @@ func (s *MessengerSuite) TestLastSentField() {
s.NotEqual(uint64(0), rawMessage.LastSent, "rawMessage.LastSent should be non-zero after sending")
}
func (s *MessengerSuite) TestShouldResendEmoji() {
// shouldn't try to resend non-emoji messages.
ok, err := shouldResendMessage(&common.RawMessage{
MessageType: protobuf.ApplicationMetadataMessage_CONTACT_UPDATE,
Sent: false,
SendCount: 2,
}, s.m.getTimesource())
s.Error(err)
s.False(ok)
// TODO rewrite test to use persistence calls, as shouldResendMessage does not contain
// relevant logic now
// func (s *MessengerSuite) TestShouldResendEmoji() {
// // shouldn't try to resend non-emoji messages.
// ok, err := s.m.shouldResendMessage(&common.RawMessage{
// MessageType: protobuf.ApplicationMetadataMessage_CONTACT_UPDATE,
// Sent: false,
// SendCount: 2,
// }, s.m.getTimesource())
// s.Error(err)
// s.False(ok)
// shouldn't try to resend already sent message
ok, err = shouldResendMessage(&common.RawMessage{
MessageType: protobuf.ApplicationMetadataMessage_EMOJI_REACTION,
Sent: true,
SendCount: 1,
}, s.m.getTimesource())
s.Error(err)
s.False(ok)
// // shouldn't try to resend already sent message
// ok, err = s.m.shouldResendMessage(&common.RawMessage{
// MessageType: protobuf.ApplicationMetadataMessage_EMOJI_REACTION,
// Sent: true,
// SendCount: 1,
// }, s.m.getTimesource())
// s.Error(err)
// s.False(ok)
// messages that already sent to many times shouldn't be resend
ok, err = shouldResendMessage(&common.RawMessage{
MessageType: protobuf.ApplicationMetadataMessage_EMOJI_REACTION,
Sent: false,
SendCount: messageResendMaxCount + 1,
}, s.m.getTimesource())
s.NoError(err)
s.False(ok)
// // messages that already sent to many times shouldn't be resend
// ok, err = s.m.shouldResendMessage(&common.RawMessage{
// MessageType: protobuf.ApplicationMetadataMessage_EMOJI_REACTION,
// Sent: false,
// SendCount: s.m.config.messageResendMaxCount + 1,
// }, s.m.getTimesource())
// s.NoError(err)
// s.False(ok)
// message sent one time CAN'T be resend in 15 seconds (only after 30)
ok, err = shouldResendMessage(&common.RawMessage{
MessageType: protobuf.ApplicationMetadataMessage_EMOJI_REACTION,
Sent: false,
SendCount: 1,
LastSent: s.m.getTimesource().GetCurrentTime() - 15*uint64(time.Second.Milliseconds()),
}, s.m.getTimesource())
s.NoError(err)
s.False(ok)
// // message sent one time CAN'T be resend in 15 seconds (only after 30)
// ok, err = s.m.shouldResendMessage(&common.RawMessage{
// MessageType: protobuf.ApplicationMetadataMessage_EMOJI_REACTION,
// Sent: false,
// SendCount: 1,
// LastSent: s.m.getTimesource().GetCurrentTime() - 15*uint64(time.Second.Milliseconds()),
// }, s.m.getTimesource())
// s.NoError(err)
// s.False(ok)
// message sent one time CAN be resend in 35 seconds
ok, err = shouldResendMessage(&common.RawMessage{
MessageType: protobuf.ApplicationMetadataMessage_EMOJI_REACTION,
Sent: false,
SendCount: 1,
LastSent: s.m.getTimesource().GetCurrentTime() - 35*uint64(time.Second.Milliseconds()),
}, s.m.getTimesource())
s.NoError(err)
s.True(ok)
// // message sent one time CAN be resend in 35 seconds
// ok, err = s.m.shouldResendMessage(&common.RawMessage{
// MessageType: protobuf.ApplicationMetadataMessage_EMOJI_REACTION,
// Sent: false,
// SendCount: 1,
// LastSent: s.m.getTimesource().GetCurrentTime() - 35*uint64(time.Second.Milliseconds()),
// }, s.m.getTimesource())
// s.NoError(err)
// s.True(ok)
// message sent three times CAN'T be resend in 100 seconds (only after 120)
ok, err = shouldResendMessage(&common.RawMessage{
MessageType: protobuf.ApplicationMetadataMessage_EMOJI_REACTION,
Sent: false,
SendCount: 3,
LastSent: s.m.getTimesource().GetCurrentTime() - 100*uint64(time.Second.Milliseconds()),
}, s.m.getTimesource())
s.NoError(err)
s.False(ok)
// // message sent three times CAN'T be resend in 100 seconds (only after 120)
// ok, err = s.m.shouldResendMessage(&common.RawMessage{
// MessageType: protobuf.ApplicationMetadataMessage_EMOJI_REACTION,
// Sent: false,
// SendCount: 3,
// LastSent: s.m.getTimesource().GetCurrentTime() - 100*uint64(time.Second.Milliseconds()),
// }, s.m.getTimesource())
// s.NoError(err)
// s.False(ok)
// message sent tow times CAN be resend in 65 seconds
ok, err = shouldResendMessage(&common.RawMessage{
MessageType: protobuf.ApplicationMetadataMessage_EMOJI_REACTION,
Sent: false,
SendCount: 3,
LastSent: s.m.getTimesource().GetCurrentTime() - 125*uint64(time.Second.Milliseconds()),
}, s.m.getTimesource())
s.NoError(err)
s.True(ok)
}
// // message sent tow times CAN be resend in 65 seconds
// ok, err = s.m.shouldResendMessage(&common.RawMessage{
// MessageType: protobuf.ApplicationMetadataMessage_EMOJI_REACTION,
// Sent: false,
// SendCount: 3,
// LastSent: s.m.getTimesource().GetCurrentTime() - 125*uint64(time.Second.Milliseconds()),
// }, s.m.getTimesource())
// s.NoError(err)
// s.True(ok)
// }
func (s *MessengerSuite) TestSendMessageWithPreviews() {
httpServer, err := server.NewMediaServer(s.m.database, nil, nil)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
CREATE INDEX idx_resend_automatically ON raw_messages(resend_automatically);

View File

@ -856,7 +856,7 @@ func (db sqlitePersistence) ExpiredMessagesIDs(maxSendCount int) ([]string, erro
FROM
raw_messages
WHERE
message_type IN (?, ?) AND sent = ? AND send_count <= ?`,
(message_type IN (?, ?) OR resend_automatically) AND sent = ? AND send_count <= ?`,
protobuf.ApplicationMetadataMessage_CHAT_MESSAGE,
protobuf.ApplicationMetadataMessage_EMOJI_REACTION,
false,

View File

@ -736,6 +736,7 @@ func TestMessagesIDsByType(t *testing.T) {
}
func TestExpiredMessagesIDs(t *testing.T) {
messageResendMaxCount := 30
db, err := openTestDB()
require.NoError(t, err)
p := newSQLitePersistence(db)

View File

@ -89,6 +89,7 @@ type settings struct {
EnableDiscV5 bool // Indicates whether discv5 is enabled or not
DefaultPubsubTopic string // Pubsub topic to be used by default for messages that do not have a topic assigned (depending whether sharding is used or not)
Options []node.WakuNodeOption
SkipPublishToTopic bool // used in testing
}
type ITelemetryClient interface {
@ -1013,6 +1014,10 @@ func (w *Waku) UnsubscribeMany(ids []string) error {
return nil
}
func (w *Waku) SkipPublishToTopic(value bool) {
w.settings.SkipPublishToTopic = value
}
func (w *Waku) broadcast() {
for {
select {
@ -1020,8 +1025,11 @@ func (w *Waku) broadcast() {
pubsubTopic := envelope.PubsubTopic()
var err error
logger := w.logger.With(zap.String("envelopeHash", hexutil.Encode(envelope.Hash())), zap.String("pubsubTopic", pubsubTopic), zap.String("contentTopic", envelope.Message().ContentTopic), zap.Int64("timestamp", envelope.Message().Timestamp))
if w.settings.LightClient {
logger.Info("publishing message via lightpush")
// For now only used in testing to simulate going offline
if w.settings.SkipPublishToTopic {
err = errors.New("Test send failure")
} else if w.settings.LightClient {
w.logger.Info("publishing message via lightpush")
_, err = w.node.Lightpush().Publish(w.ctx, envelope.Message(), lightpush.WithPubSubTopic(pubsubTopic))
} else {
logger.Info("publishing message via relay")