package console import ( "bytes" "context" "database/sql" "fmt" "runtime" "strconv" ) // errOuterStoreInView is a call on the outer, captured Store from inside the // callback withClaimSQL is running (review m-5). The view holds the pool's // only connection, so the call could only wait for its own caller; it is // refused at once instead, before any remote check. var errOuterStoreInView = fmt.Errorf("%w: the outer Store was used inside withClaimSQL; use the view it passes", ErrClaimSQLScope) // goroutineID is the running goroutine's number. It is used only to tell // whether the caller is the goroutine that holds the bound view; a call from // any other goroutine waits for the connection as before. func goroutineID() uint64 { var buf [64]byte b := bytes.TrimPrefix(buf[:runtime.Stack(buf[:], false)], []byte("goroutine ")) if i := bytes.IndexByte(b, ' '); i > 0 { b = b[:i] } id, _ := strconv.ParseUint(string(b), 10, 64) return id } // insideView reports whether the calling goroutine holds the bound view. func (g *claimGate) insideView() bool { h := g.viewG.Load() return h != 0 && h == goroutineID() } // outerInsideView reports a call on the outer Store from the goroutine that // holds its bound view. A goroutine fn starts is not caught: it waits. func (s *Store) outerInsideView() bool { return s.gate != nil && s.bound == nil && s.gate.insideView() } // claimOuterDB is the gated Store's own handle. It is the pool, except that // the goroutine holding the bound view is refused instead of waiting on the // connection it already holds. type claimOuterDB struct { *sql.DB g *claimGate } var _ sqlHandle = claimOuterDB{} // refusedContext is already done with errOuterStoreInView, so database/sql // refuses before it waits for a connection. It is how QueryRow, which has no // error return, carries the refusal to Scan. type refusedContext struct{ context.Context } var closedDone = func() chan struct{} { c := make(chan struct{}); close(c); return c }() func (refusedContext) Done() <-chan struct{} { return closedDone } func (refusedContext) Err() error { return errOuterStoreInView } func (o claimOuterDB) guard(ctx context.Context) context.Context { if o.g.insideView() { return refusedContext{ctx} } return ctx } func (o claimOuterDB) Exec(query string, args ...any) (sql.Result, error) { return o.ExecContext(context.Background(), query, args...) } func (o claimOuterDB) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) { return o.DB.ExecContext(o.guard(ctx), query, args...) } func (o claimOuterDB) Query(query string, args ...any) (*sql.Rows, error) { return o.QueryContext(context.Background(), query, args...) } func (o claimOuterDB) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) { return o.DB.QueryContext(o.guard(ctx), query, args...) } func (o claimOuterDB) QueryRow(query string, args ...any) *sql.Row { return o.QueryRowContext(context.Background(), query, args...) } func (o claimOuterDB) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row { return o.DB.QueryRowContext(o.guard(ctx), query, args...) } func (o claimOuterDB) Begin() (*sql.Tx, error) { return o.BeginTx(context.Background(), nil) } func (o claimOuterDB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) { return o.DB.BeginTx(o.guard(ctx), opts) } func (o claimOuterDB) Prepare(query string) (*sql.Stmt, error) { return o.PrepareContext(context.Background(), query) } func (o claimOuterDB) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) { return o.DB.PrepareContext(o.guard(ctx), query) }