From e2c8a1930affe0c6be42ec7dc7191da8a6f5c860 Mon Sep 17 00:00:00 2001 From: Deluan Date: Fri, 4 Sep 2026 22:25:13 -0400 Subject: [PATCH] fix(pglite): do not cut slow readers, and bind playlist rules as text The bridge gave every reply write a 5 second deadline. Scanner phase 3 streams all touched albums through a cursor and runs queries per row on other pooled connections, which queue on the single session, so a large library could not drain the reply in time and the bridge closed the pipe mid-message ('unexpected EOF'). Replies now block as long as the client needs, and client connections are closed on shutdown instead. Playlist rules were bound as []byte, which pgx sends as bytea; the text column then stored the hex form and the JSON parser failed on it. --- db/pglite/pglite.go | 4 +++- persistence/playlist_repository.go | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/db/pglite/pglite.go b/db/pglite/pglite.go index a329e5fa3..bf780be14 100644 --- a/db/pglite/pglite.go +++ b/db/pglite/pglite.go @@ -428,6 +428,7 @@ func (pg *PGlite) handleConn(conn net.Conn, ioBase string) { defer fmt.Fprintln(pg.cfg.Stderr, "# bridge: client disconnected") } defer conn.Close() + defer context.AfterFunc(pg.ctx, func() { _ = conn.Close() })() outFile := ioBase + ".out" buf := make([]byte, 65536) @@ -597,7 +598,8 @@ func (pg *PGlite) sendReplies(conn net.Conn, replies [][]byte) bool { fmt.Fprintf(pg.cfg.Stderr, "# bridge S> raw %q\n", data) } } - _ = conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) + // No write deadline: a client may drain a large result slowly (e.g. a cursor that runs + // queries per row); the connection is closed on shutdown instead. if _, err := conn.Write(data); err != nil { return false } diff --git a/persistence/playlist_repository.go b/persistence/playlist_repository.go index af2a89aa3..28ab9af1d 100644 --- a/persistence/playlist_repository.go +++ b/persistence/playlist_repository.go @@ -33,12 +33,12 @@ func (p *dbPlaylist) PostScan() error { } func (p dbPlaylist) PostMapArgs(args map[string]any) error { - var err error if p.Playlist.IsSmartPlaylist() { - args["rules"], err = json.Marshal(p.Playlist.Rules) + rules, err := json.Marshal(p.Playlist.Rules) if err != nil { return fmt.Errorf("invalid criteria expression: %w", err) } + args["rules"] = string(rules) // []byte would be bound as bytea and stored as its hex form // Smart playlist counters are owned by refreshCounters (evaluation), never by callers delete(args, "song_count") delete(args, "duration")