github.com/status-im/status-go@v1.1.0/protocol/anonmetrics/client.go (about) 1 package anonmetrics 2 3 import ( 4 "context" 5 "crypto/ecdsa" 6 "errors" 7 "sync" 8 "time" 9 10 "github.com/golang/protobuf/proto" 11 "go.uber.org/zap" 12 13 "github.com/status-im/status-go/appmetrics" 14 "github.com/status-im/status-go/eth-node/crypto" 15 "github.com/status-im/status-go/protocol/common" 16 "github.com/status-im/status-go/protocol/protobuf" 17 ) 18 19 const ActiveClientPhrase = "yes i am wanting the activation of the anon metrics client, please thank you lots thank you" 20 21 type ClientConfig struct { 22 ShouldSend bool 23 SendAddress *ecdsa.PublicKey 24 Active string 25 } 26 27 type Client struct { 28 Config *ClientConfig 29 DB *appmetrics.Database 30 Identity *ecdsa.PrivateKey 31 Logger *zap.Logger 32 33 //messageSender is a message processor used to send metric batch messages 34 messageSender *common.MessageSender 35 36 IntervalInc *FibonacciIntervalIncrementer 37 38 // mainLoopQuit is a channel that concurrently orchestrates that the main loop that should be terminated 39 mainLoopQuit chan struct{} 40 41 // deleteLoopQuit is a channel that concurrently orchestrates that the delete loop that should be terminated 42 deleteLoopQuit chan struct{} 43 44 // DBLock prevents deletion of DB items during mainloop 45 DBLock sync.Mutex 46 } 47 48 func NewClient(sender *common.MessageSender) *Client { 49 return &Client{ 50 messageSender: sender, 51 IntervalInc: &FibonacciIntervalIncrementer{ 52 Last: 0, 53 Current: 1, 54 }, 55 } 56 } 57 58 func (c *Client) sendUnprocessedMetrics() { 59 if c.Config.Active != ActiveClientPhrase { 60 return 61 } 62 63 c.Logger.Debug("sendUnprocessedMetrics() triggered") 64 65 c.DBLock.Lock() 66 defer c.DBLock.Unlock() 67 68 // Get all unsent metrics grouped by session id 69 uam, err := c.DB.GetUnprocessedGroupedBySession() 70 if err != nil { 71 c.Logger.Error("failed to get unprocessed messages grouped by session", zap.Error(err)) 72 } 73 c.Logger.Debug("unprocessed metrics from db", zap.Reflect("uam", uam)) 74 75 for session, batch := range uam { 76 c.Logger.Debug("processing uam from session", zap.String("session", session)) 77 78 // Convert the metrics into protobuf 79 amb, err := adaptModelsToProtoBatch(batch, &c.Identity.PublicKey) 80 if err != nil { 81 c.Logger.Error("failed to adapt models to protobuf batch", zap.Error(err)) 82 return 83 } 84 85 // Generate an ephemeral key per session id 86 ephemeralKey, err := crypto.GenerateKey() 87 if err != nil { 88 c.Logger.Error("failed to generate an ephemeral key", zap.Error(err)) 89 return 90 } 91 92 // Prepare the protobuf message 93 encodedMessage, err := proto.Marshal(amb) 94 if err != nil { 95 c.Logger.Error("failed to marshal protobuf", zap.Error(err)) 96 return 97 } 98 rawMessage := common.RawMessage{ 99 Payload: encodedMessage, 100 Sender: ephemeralKey, 101 SkipEncryptionLayer: true, 102 SendOnPersonalTopic: true, 103 MessageType: protobuf.ApplicationMetadataMessage_ANONYMOUS_METRIC_BATCH, 104 } 105 106 c.Logger.Debug("rawMessage prepared from unprocessed anonymous metrics", zap.Reflect("rawMessage", rawMessage)) 107 108 // Send the metrics batch 109 _, err = c.messageSender.SendPrivate(context.Background(), c.Config.SendAddress, &rawMessage) 110 if err != nil { 111 c.Logger.Error("failed to send metrics batch message", zap.Error(err)) 112 return 113 } 114 115 // Mark metrics as processed 116 err = c.DB.SetToProcessed(batch) 117 if err != nil { 118 c.Logger.Error("failed to set metrics as processed in db", zap.Error(err)) 119 } 120 } 121 } 122 123 func (c *Client) mainLoop() error { 124 if c.Config.Active != ActiveClientPhrase { 125 return nil 126 } 127 128 c.Logger.Debug("mainLoop() triggered") 129 130 for { 131 c.sendUnprocessedMetrics() 132 133 waitFor := time.Duration(c.IntervalInc.Next()) * time.Second 134 c.Logger.Debug("mainLoop() wait interval set", zap.Duration("waitFor", waitFor)) 135 select { 136 case <-time.After(waitFor): 137 case <-c.mainLoopQuit: 138 return nil 139 } 140 } 141 } 142 143 func (c *Client) startMainLoop() { 144 if c.Config.Active != ActiveClientPhrase { 145 return 146 } 147 148 c.Logger.Debug("startMainLoop() triggered") 149 150 c.stopMainLoop() 151 c.mainLoopQuit = make(chan struct{}) 152 go func() { 153 c.Logger.Debug("startMainLoop() anonymous go routine triggered") 154 err := c.mainLoop() 155 if err != nil { 156 c.Logger.Error("main loop exited with an error", zap.Error(err)) 157 } 158 }() 159 } 160 161 func (c *Client) deleteLoop() error { 162 // Sleep to give the main lock time to process any old messages 163 time.Sleep(time.Second * 10) 164 165 for { 166 func() { 167 c.DBLock.Lock() 168 defer c.DBLock.Unlock() 169 170 oneWeekAgo := time.Now().Add(time.Hour * 24 * 7 * -1) 171 err := c.DB.DeleteOlderThan(&oneWeekAgo) 172 if err != nil { 173 c.Logger.Error("failed to delete metrics older than given time", 174 zap.Time("time given", oneWeekAgo), 175 zap.Error(err)) 176 } 177 }() 178 179 select { 180 case <-time.After(time.Hour): 181 case <-c.deleteLoopQuit: 182 return nil 183 } 184 } 185 } 186 187 func (c *Client) startDeleteLoop() { 188 c.stopDeleteLoop() 189 c.deleteLoopQuit = make(chan struct{}) 190 go func() { 191 err := c.deleteLoop() 192 if err != nil { 193 c.Logger.Error("delete loop exited with an error", zap.Error(err)) 194 } 195 }() 196 } 197 198 func (c *Client) Start() error { 199 c.Logger.Debug("Main Start() triggered") 200 if c.messageSender == nil { 201 return errors.New("can't start, missing message processor") 202 } 203 204 c.startMainLoop() 205 c.startDeleteLoop() 206 return nil 207 } 208 209 func (c *Client) stopMainLoop() { 210 c.Logger.Debug("stopMainLoop() triggered") 211 212 if c.mainLoopQuit != nil { 213 c.Logger.Debug("mainLoopQuit not set, attempting to close") 214 215 close(c.mainLoopQuit) 216 c.mainLoopQuit = nil 217 } 218 } 219 220 func (c *Client) stopDeleteLoop() { 221 if c.deleteLoopQuit != nil { 222 close(c.deleteLoopQuit) 223 c.deleteLoopQuit = nil 224 } 225 } 226 227 func (c *Client) Stop() error { 228 c.stopMainLoop() 229 c.stopDeleteLoop() 230 return nil 231 }