Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).

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.

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