Monorepo for Tangled tangled.org

jetstream: fix reconnect logic and last time_us saving

Reconnection is now handed by cancelling the connCtx context. This is a
lot cleaner and the jetstream client package honors context
cancellation.

Saving of last time_us is also simplified now. We only save (update,
rather) after the last seen event by incrementing the event's time_us by
1 (this is ok since it's a monotonic clock). We pick up from here upon
reconnect and don't save last time_us for any other reason.

Lastly, outside of first boot, we should only ever use UpdateLastTimeUs.

Changed files
+76 -44
appview
cmd
knotserver
jetstream
knotserver
+9 -1
appview/db/jetstream.go
··· 1 1 package db 2 2 3 3 func (d *DB) SaveLastTimeUs(lastTimeUs int64) error { 4 - _, err := d.db.Exec(`update _jetstream set last_time_us = ?`, lastTimeUs) 4 + _, err := d.db.Exec(`insert into _jetstream (last_time_us) values (?)`, lastTimeUs) 5 5 return err 6 + } 7 + 8 + func (d *DB) UpdateLastTimeUs(lastTimeUs int64) error { 9 + _, err := d.db.Exec(`update _jetstream set last_time_us = ? where rowid = 1`, lastTimeUs) 10 + if err != nil { 11 + return err 12 + } 13 + return nil 6 14 } 7 15 8 16 func (d *DB) GetLastTimeUs() (int64, error) {
+1 -1
appview/state/middleware.go
··· 154 154 id, err := s.resolver.ResolveIdent(req.Context(), didOrHandle) 155 155 if err != nil { 156 156 // invalid did or handle 157 - log.Println("failed to resolve did/handle") 157 + log.Println("failed to resolve did/handle:", err) 158 158 w.WriteHeader(http.StatusNotFound) 159 159 return 160 160 }
+1 -1
appview/state/settings.go
··· 68 68 // invalid record 69 69 if err != nil { 70 70 log.Printf("failed to create record: %s", err) 71 - s.pages.Notice(w, "settings-keys-bad", "Failed to create record.") 71 + s.pages.Notice(w, "settings-keys", "Failed to create record.") 72 72 return 73 73 } 74 74
+4 -2
appview/state/state.go
··· 8 8 "encoding/json" 9 9 "fmt" 10 10 "log" 11 + "log/slog" 11 12 "net/http" 12 13 "strings" 13 14 "time" ··· 59 60 60 61 resolver := appview.NewResolver() 61 62 62 - jc, err := jetstream.NewJetstreamClient("appview", []string{tangled.GraphFollowNSID}, nil, db, false) 63 + jc, err := jetstream.NewJetstreamClient("appview", []string{tangled.GraphFollowNSID}, nil, slog.Default(), db, false) 63 64 if err != nil { 64 65 return nil, fmt.Errorf("failed to create jetstream client: %w", err) 65 66 } ··· 83 84 if err != nil { 84 85 return fmt.Errorf("failed to add follow to db: %w", err) 85 86 } 86 - return db.SaveLastTimeUs(e.TimeUS) 87 + return db.UpdateLastTimeUs(e.TimeUS) 87 88 } 88 89 89 90 return nil ··· 125 126 126 127 resolved, err := s.resolver.ResolveIdent(ctx, handle) 127 128 if err != nil { 129 + log.Println("failed to resolve handle:", err) 128 130 s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle)) 129 131 return 130 132 }
+1 -1
cmd/knotserver/main.go
··· 45 45 jc, err := jetstream.NewJetstreamClient("knotserver", []string{ 46 46 tangled.PublicKeyNSID, 47 47 tangled.KnotMemberNSID, 48 - }, nil, db, true) 48 + }, nil, l, db, true) 49 49 if err != nil { 50 50 l.Error("failed to setup jetstream", "error", err) 51 51 }
+44 -29
jetstream/jetstream.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 + "log/slog" 6 7 "sync" 7 8 "time" 8 9 ··· 15 16 type DB interface { 16 17 GetLastTimeUs() (int64, error) 17 18 SaveLastTimeUs(int64) error 19 + UpdateLastTimeUs(int64) error 18 20 } 19 21 20 22 type JetstreamClient struct { 21 23 cfg *client.ClientConfig 22 24 client *client.Client 23 25 ident string 26 + l *slog.Logger 24 27 25 - db DB 26 - reconnectCh chan struct{} 27 - waitForDid bool 28 - mu sync.RWMutex 28 + db DB 29 + waitForDid bool 30 + mu sync.RWMutex 31 + 32 + cancel context.CancelFunc 33 + cancelMu sync.Mutex 29 34 } 30 35 31 36 func (j *JetstreamClient) AddDid(did string) { ··· 35 40 j.mu.Lock() 36 41 j.cfg.WantedDids = append(j.cfg.WantedDids, did) 37 42 j.mu.Unlock() 38 - j.reconnectCh <- struct{}{} 39 43 } 40 44 41 45 func (j *JetstreamClient) UpdateDids(dids []string) { 42 46 j.mu.Lock() 43 47 for _, did := range dids { 44 48 if did != "" { 49 + j.cfg.WantedDids = append(j.cfg.WantedDids, did) 45 50 } 46 - j.cfg.WantedDids = append(j.cfg.WantedDids, did) 47 51 } 48 52 j.mu.Unlock() 49 - j.reconnectCh <- struct{}{} 53 + 54 + j.cancelMu.Lock() 55 + if j.cancel != nil { 56 + j.cancel() 57 + } 58 + j.cancelMu.Unlock() 50 59 } 51 60 52 - func NewJetstreamClient(ident string, collections []string, cfg *client.ClientConfig, db DB, waitForDid bool) (*JetstreamClient, error) { 61 + func NewJetstreamClient(ident string, collections []string, cfg *client.ClientConfig, logger *slog.Logger, db DB, waitForDid bool) (*JetstreamClient, error) { 53 62 if cfg == nil { 54 63 cfg = client.DefaultClientConfig() 55 64 cfg.WebsocketURL = "wss://jetstream1.us-west.bsky.network/subscribe" ··· 60 69 cfg: cfg, 61 70 ident: ident, 62 71 db: db, 72 + l: logger, 63 73 64 74 // This will make the goroutine in StartJetstream wait until 65 75 // cfg.WantedDids has been populated, typically using UpdateDids. 66 - waitForDid: waitForDid, 67 - reconnectCh: make(chan struct{}, 1), 76 + waitForDid: waitForDid, 68 77 }, nil 69 78 } 70 79 71 80 // StartJetstream starts the jetstream client and processes events using the provided processFunc. 72 81 // The caller is responsible for saving the last time_us to the database (just use your db.SaveLastTimeUs). 73 82 func (j *JetstreamClient) StartJetstream(ctx context.Context, processFunc func(context.Context, *models.Event) error) error { 74 - logger := log.FromContext(ctx) 83 + logger := j.l 75 84 76 85 sched := sequential.NewScheduler(j.ident, logger, processFunc) 77 86 ··· 82 91 j.client = client 83 92 84 93 go func() { 85 - lastTimeUs := j.getLastTimeUs(ctx) 86 94 if j.waitForDid { 87 95 for len(j.cfg.WantedDids) == 0 { 88 96 time.Sleep(time.Second) 89 97 } 90 98 } 91 99 logger.Info("done waiting for did") 92 - j.connectAndRead(ctx, &lastTimeUs) 100 + j.connectAndRead(ctx) 93 101 }() 94 102 95 103 return nil 96 104 } 97 105 98 - func (j *JetstreamClient) connectAndRead(ctx context.Context, cursor *int64) { 106 + func (j *JetstreamClient) connectAndRead(ctx context.Context) { 99 107 l := log.FromContext(ctx) 100 108 for { 109 + cursor := j.getLastTimeUs(ctx) 110 + 111 + connCtx, cancel := context.WithCancel(ctx) 112 + j.cancelMu.Lock() 113 + j.cancel = cancel 114 + j.cancelMu.Unlock() 115 + 116 + if err := j.client.ConnectAndRead(connCtx, cursor); err != nil { 117 + l.Error("error reading jetstream", "error", err) 118 + } 119 + 101 120 select { 102 - case <-j.reconnectCh: 103 - l.Info("(re)connecting jetstream client") 104 - j.client.Scheduler.Shutdown() 105 - if err := j.client.ConnectAndRead(ctx, cursor); err != nil { 106 - l.Error("error reading jetstream", "error", err) 107 - } 108 - default: 109 - if err := j.client.ConnectAndRead(ctx, cursor); err != nil { 110 - l.Error("error reading jetstream", "error", err) 111 - } 121 + case <-ctx.Done(): 122 + l.Info("context done, stopping jetstream") 123 + return 124 + case <-connCtx.Done(): 125 + l.Info("connection context done, reconnecting") 126 + continue 112 127 } 113 128 } 114 129 } 115 130 116 - func (j *JetstreamClient) getLastTimeUs(ctx context.Context) int64 { 131 + func (j *JetstreamClient) getLastTimeUs(ctx context.Context) *int64 { 117 132 l := log.FromContext(ctx) 118 133 lastTimeUs, err := j.db.GetLastTimeUs() 119 134 if err != nil { ··· 121 136 lastTimeUs = time.Now().UnixMicro() 122 137 err = j.db.SaveLastTimeUs(lastTimeUs) 123 138 if err != nil { 124 - l.Error("failed to save last time us") 139 + l.Error("failed to save last time us", "error", err) 125 140 } 126 141 } 127 142 ··· 129 144 if time.Now().UnixMicro()-lastTimeUs > 7*24*60*60*1000*1000 { 130 145 lastTimeUs = time.Now().UnixMicro() 131 146 l.Warn("last time us is older than a week. discarding that and starting from now") 132 - err = j.db.SaveLastTimeUs(lastTimeUs) 147 + err = j.db.UpdateLastTimeUs(lastTimeUs) 133 148 if err != nil { 134 - l.Error("failed to save last time us") 149 + l.Error("failed to save last time us", "error", err) 135 150 } 136 151 } 137 152 138 153 l.Info("found last time_us", "time_us", lastTimeUs) 139 - return lastTimeUs 154 + return &lastTimeUs 140 155 }
+9 -1
knotserver/db/jetstream.go
··· 1 1 package db 2 2 3 3 func (d *DB) SaveLastTimeUs(lastTimeUs int64) error { 4 - _, err := d.db.Exec(`update _jetstream set last_time_us = ?`, lastTimeUs) 4 + _, err := d.db.Exec(`insert into _jetstream (last_time_us) values (?)`, lastTimeUs) 5 5 return err 6 + } 7 + 8 + func (d *DB) UpdateLastTimeUs(lastTimeUs int64) error { 9 + _, err := d.db.Exec(`update _jetstream set last_time_us = ? where rowid = 1`, lastTimeUs) 10 + if err != nil { 11 + return err 12 + } 13 + return nil 6 14 } 7 15 8 16 func (d *DB) GetLastTimeUs() (int64, error) {
+7 -8
knotserver/jetstream.go
··· 29 29 return nil 30 30 } 31 31 32 - func (h *Handle) processKnotMember(ctx context.Context, did string, record tangled.KnotMember) error { 32 + func (h *Handle) processKnotMember(ctx context.Context, did string, record tangled.KnotMember, eventTime int64) error { 33 33 l := log.FromContext(ctx) 34 34 35 35 if record.Domain != h.c.Server.Hostname { ··· 43 43 return fmt.Errorf("failed to enforce permissions: %w", err) 44 44 } 45 45 46 - l.Info("adding member") 47 46 if err := h.e.AddMember(ThisServer, record.Member); err != nil { 48 47 l.Error("failed to add member", "error", err) 49 48 return fmt.Errorf("failed to add member: %w", err) ··· 59 58 return fmt.Errorf("failed to fetch and add keys: %w", err) 60 59 } 61 60 61 + lastTimeUs := eventTime + 1 62 + fmt.Println("lastTimeUs", lastTimeUs) 63 + if err := h.db.UpdateLastTimeUs(lastTimeUs); err != nil { 64 + return fmt.Errorf("failed to save last time us: %w", err) 65 + } 62 66 h.jc.UpdateDids([]string{did}) 63 67 return nil 64 68 } ··· 129 133 if err := json.Unmarshal(raw, &record); err != nil { 130 134 return fmt.Errorf("failed to unmarshal record: %w", err) 131 135 } 132 - if err := h.processKnotMember(ctx, did, record); err != nil { 136 + if err := h.processKnotMember(ctx, did, record, event.TimeUS); err != nil { 133 137 return fmt.Errorf("failed to process knot member: %w", err) 134 138 } 135 - } 136 - 137 - lastTimeUs := event.TimeUS 138 - if err := h.db.SaveLastTimeUs(lastTimeUs); err != nil { 139 - return fmt.Errorf("failed to save last time us: %w", err) 140 139 } 141 140 142 141 return nil