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  }