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)