github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/realtime/redis_hub.go (about)

     1  package realtime
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"strings"
     7  
     8  	"github.com/cozy/cozy-stack/pkg/logger"
     9  	"github.com/cozy/cozy-stack/pkg/prefixer"
    10  	redis "github.com/redis/go-redis/v9"
    11  )
    12  
    13  const eventsRedisKey = "realtime:events"
    14  
    15  type redisHub struct {
    16  	c        redis.UniversalClient
    17  	ctx      context.Context
    18  	mem      *memHub
    19  	firehose *topic
    20  }
    21  
    22  func newRedisHub(c redis.UniversalClient) *redisHub {
    23  	ctx := context.Background()
    24  	firehose := newTopic()
    25  	mem := newMemHub()
    26  	hub := &redisHub{c, ctx, mem, firehose}
    27  	go hub.start()
    28  	return hub
    29  }
    30  
    31  // JSONDoc is a map representing a simple json object that implements
    32  // the couchdb.Doc interface.
    33  //
    34  // Note: we can't use the couchdb.JSONDoc as the couchdb package imports the
    35  // realtime package, and it would create an import loop. And we cannot move the
    36  // JSONDoc from couchdb here, as some of its methods use other functions from
    37  // the couchdb package.
    38  type JSONDoc struct {
    39  	M    map[string]interface{}
    40  	Type string
    41  }
    42  
    43  // ID returns the ID of the document
    44  func (j JSONDoc) ID() string { id, _ := j.M["_id"].(string); return id }
    45  
    46  // DocType returns the DocType of the document
    47  func (j JSONDoc) DocType() string { return j.Type }
    48  
    49  // MarshalJSON is used for marshalling the document to JSON, with the doctype
    50  // as _type.
    51  func (j *JSONDoc) MarshalJSON() ([]byte, error) {
    52  	m := map[string]interface{}{"_type": j.Type}
    53  	if j.M != nil {
    54  		for k, v := range j.M {
    55  			m[k] = v
    56  		}
    57  	}
    58  	return json.Marshal(m)
    59  }
    60  
    61  func toJSONDoc(d map[string]interface{}) *JSONDoc {
    62  	if d == nil {
    63  		return nil
    64  	}
    65  	doctype, _ := d["_type"].(string)
    66  	delete(d, "_type")
    67  	return &JSONDoc{d, doctype}
    68  }
    69  
    70  type jsonEvent struct {
    71  	Cluster int
    72  	Domain  string
    73  	Prefix  string
    74  	Verb    string
    75  	Doc     *JSONDoc
    76  	Old     *JSONDoc
    77  }
    78  
    79  func (j *jsonEvent) UnmarshalJSON(buf []byte) error {
    80  	var m map[string]interface{}
    81  	if err := json.Unmarshal(buf, &m); err != nil {
    82  		return err
    83  	}
    84  	if cluster, ok := m["cluster"].(float64); ok {
    85  		j.Cluster = int(cluster)
    86  	}
    87  	j.Domain, _ = m["domain"].(string)
    88  	j.Prefix, _ = m["prefix"].(string)
    89  	j.Verb, _ = m["verb"].(string)
    90  	if doc, ok := m["doc"].(map[string]interface{}); ok {
    91  		j.Doc = toJSONDoc(doc)
    92  	}
    93  	if old, ok := m["old"].(map[string]interface{}); ok {
    94  		j.Old = toJSONDoc(old)
    95  	}
    96  	return nil
    97  }
    98  
    99  func (h *redisHub) start() {
   100  	sub := h.c.Subscribe(h.ctx, eventsRedisKey)
   101  	log := logger.WithNamespace("realtime-redis")
   102  	for msg := range sub.Channel() {
   103  		parts := strings.SplitN(msg.Payload, ",", 2)
   104  		if len(parts) < 2 {
   105  			log.Warnf("Invalid payload: %s", msg.Payload)
   106  			continue
   107  		}
   108  		// We clone the doctype to allow the GC to collect the payload even if
   109  		// the jsonEvent is still in use.
   110  		doctype := strings.Clone(parts[0])
   111  		r := strings.NewReader(parts[1])
   112  		je := jsonEvent{}
   113  		if err := json.NewDecoder(r).Decode(&je); err != nil {
   114  			log.Warnf("Error on start: %s", err)
   115  			continue
   116  		}
   117  		if je.Doc != nil {
   118  			je.Doc.Type = doctype
   119  		}
   120  		if je.Old != nil {
   121  			je.Old.Type = doctype
   122  		}
   123  		db := prefixer.NewPrefixer(je.Cluster, je.Domain, je.Prefix)
   124  		h.mem.Publish(db, je.Verb, je.Doc, je.Old)
   125  	}
   126  	logger.WithNamespace("realtime-redis").Infof("End of subscribe channel")
   127  }
   128  
   129  func (h *redisHub) Publish(db prefixer.Prefixer, verb string, doc, oldDoc Doc) {
   130  	e := newEvent(db, verb, doc, oldDoc)
   131  	h.firehose.broadcast <- e
   132  	buf, err := json.Marshal(e)
   133  	if err != nil {
   134  		log := logger.WithNamespace("realtime-redis")
   135  		log.Warnf("Error on publish: %s", err)
   136  		return
   137  	}
   138  	h.c.Publish(h.ctx, eventsRedisKey, e.Doc.DocType()+","+string(buf))
   139  }
   140  
   141  func (h *redisHub) Subscriber(db prefixer.Prefixer) *Subscriber {
   142  	return h.mem.Subscriber(db)
   143  }
   144  
   145  func (h *redisHub) SubscribeFirehose() *Subscriber {
   146  	sub := newSubscriber(h, globalPrefixer)
   147  	h.firehose.subscribe <- &toWatch{sub, ""}
   148  	return sub
   149  }
   150  
   151  func (h *redisHub) subscribe(sub *Subscriber, key string) {
   152  	panic("not reachable code")
   153  }
   154  
   155  func (h *redisHub) unsubscribe(sub *Subscriber, key string) {
   156  	h.firehose.unsubscribe <- &toWatch{sub, ""}
   157  	<-h.firehose.running
   158  }
   159  
   160  func (h *redisHub) watch(sub *Subscriber, key, id string) {
   161  	panic("not reachable code")
   162  }
   163  
   164  func (h *redisHub) unwatch(sub *Subscriber, key, id string) {
   165  	panic("not reachable code")
   166  }
   167  
   168  func (h *redisHub) close(sub *Subscriber) {
   169  	h.unsubscribe(sub, "*")
   170  }
   171  
   172  var _ Doc = (*JSONDoc)(nil)