Skip to content

Commit f512b96

Browse files
committed
Add PgConn.SyncConn
This provides a way to ensure it is safe to directly read or write to the underlying net.Conn. #1673
1 parent 05440f9 commit f512b96

File tree

4 files changed

+63
-24
lines changed

4 files changed

+63
-24
lines changed

pgconn/internal/bgreader/bgreader.go

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@ import (
99
)
1010

1111
const (
12-
bgReaderStatusStopped = iota
13-
bgReaderStatusRunning
14-
bgReaderStatusStopping
12+
StatusStopped = iota
13+
StatusRunning
14+
StatusStopping
1515
)
1616

1717
// BGReader is an io.Reader that can optionally buffer reads in the background. It is safe for concurrent use.
1818
type BGReader struct {
1919
r io.Reader
2020

21-
cond *sync.Cond
22-
bgReaderStatus int32
23-
readResults []readResult
21+
cond *sync.Cond
22+
status int32
23+
readResults []readResult
2424
}
2525

2626
type readResult struct {
@@ -34,14 +34,14 @@ func (r *BGReader) Start() {
3434
r.cond.L.Lock()
3535
defer r.cond.L.Unlock()
3636

37-
switch r.bgReaderStatus {
38-
case bgReaderStatusStopped:
39-
r.bgReaderStatus = bgReaderStatusRunning
37+
switch r.status {
38+
case StatusStopped:
39+
r.status = StatusRunning
4040
go r.bgRead()
41-
case bgReaderStatusRunning:
41+
case StatusRunning:
4242
// no-op
43-
case bgReaderStatusStopping:
44-
r.bgReaderStatus = bgReaderStatusRunning
43+
case StatusStopping:
44+
r.status = StatusRunning
4545
}
4646
}
4747

@@ -51,16 +51,23 @@ func (r *BGReader) Stop() {
5151
r.cond.L.Lock()
5252
defer r.cond.L.Unlock()
5353

54-
switch r.bgReaderStatus {
55-
case bgReaderStatusStopped:
54+
switch r.status {
55+
case StatusStopped:
5656
// no-op
57-
case bgReaderStatusRunning:
58-
r.bgReaderStatus = bgReaderStatusStopping
59-
case bgReaderStatusStopping:
57+
case StatusRunning:
58+
r.status = StatusStopping
59+
case StatusStopping:
6060
// no-op
6161
}
6262
}
6363

64+
// Status returns the current status of the background reader.
65+
func (r *BGReader) Status() int32 {
66+
r.cond.L.Lock()
67+
defer r.cond.L.Unlock()
68+
return r.status
69+
}
70+
6471
func (r *BGReader) bgRead() {
6572
keepReading := true
6673
for keepReading {
@@ -70,8 +77,8 @@ func (r *BGReader) bgRead() {
7077

7178
r.cond.L.Lock()
7279
r.readResults = append(r.readResults, readResult{buf: buf, err: err})
73-
if r.bgReaderStatus == bgReaderStatusStopping || err != nil {
74-
r.bgReaderStatus = bgReaderStatusStopped
80+
if r.status == StatusStopping || err != nil {
81+
r.status = StatusStopped
7582
keepReading = false
7683
}
7784
r.cond.L.Unlock()
@@ -89,7 +96,7 @@ func (r *BGReader) Read(p []byte) (int, error) {
8996
}
9097

9198
// There are no unread background read results and the background reader is stopped.
92-
if r.bgReaderStatus == bgReaderStatusStopped {
99+
if r.status == StatusStopped {
93100
return r.r.Read(p)
94101
}
95102

pgconn/pgconn.go

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,8 @@ func (pgConn *PgConn) receiveMessage() (pgproto3.BackendMessage, error) {
556556
return msg, nil
557557
}
558558

559-
// Conn returns the underlying net.Conn. This rarely necessary.
559+
// Conn returns the underlying net.Conn. This rarely necessary. If the connection will be directly used for reading or
560+
// writing then SyncConn should usually be called before Conn.
560561
func (pgConn *PgConn) Conn() net.Conn {
561562
return pgConn.conn
562563
}
@@ -1740,6 +1741,30 @@ func (pgConn *PgConn) flushWithPotentialWriteReadDeadlock() error {
17401741
return err
17411742
}
17421743

1744+
// SyncConn prepares the underlying net.Conn for direct use. PgConn may internally buffer reads or use goroutines for
1745+
// background IO. This means that any direct use of the underlying net.Conn may be corrupted if a read is already
1746+
// buffered or a read is in progress. SyncConn drains read buffers and stops background IO. In some cases this may
1747+
// require sending a ping to the server. ctx can be used to cancel this operation. This should be called before any
1748+
// operation that will use the underlying net.Conn directly. e.g. Before Conn() or Hijack().
1749+
//
1750+
// This should not be confused with the PostgreSQL protocol Sync message.
1751+
func (pgConn *PgConn) SyncConn(ctx context.Context) error {
1752+
for i := 0; i < 10; i++ {
1753+
if pgConn.bgReader.Status() == bgreader.StatusStopped && pgConn.frontend.ReadBufferLen() == 0 {
1754+
return nil
1755+
}
1756+
1757+
err := pgConn.Ping(ctx)
1758+
if err != nil {
1759+
return fmt.Errorf("SyncConn: Ping failed while syncing conn: %w", err)
1760+
}
1761+
}
1762+
1763+
// This should never happen. Only way I can imagine this occuring is if the server is constantly sending data such as
1764+
// LISTEN/NOTIFY or log notifications such that we never can get an empty buffer.
1765+
return errors.New("SyncConn: conn never synchronized")
1766+
}
1767+
17431768
// HijackedConn is the result of hijacking a connection.
17441769
//
17451770
// Due to the necessary exposure of internal implementation details, it is not covered by the semantic versioning
@@ -1754,9 +1779,9 @@ type HijackedConn struct {
17541779
Config *Config
17551780
}
17561781

1757-
// Hijack extracts the internal connection data. pgConn must be in an idle state. pgConn is unusable after hijacking.
1758-
// Hijacking is typically only useful when using pgconn to establish a connection, but taking complete control of the
1759-
// raw connection after that (e.g. a load balancer or proxy).
1782+
// Hijack extracts the internal connection data. pgConn must be in an idle state. SyncConn should be called immediately
1783+
// before Hijack. pgConn is unusable after hijacking. Hijacking is typically only useful when using pgconn to establish
1784+
// a connection, but taking complete control of the raw connection after that (e.g. a load balancer or proxy).
17601785
//
17611786
// Due to the necessary exposure of internal implementation details, it is not covered by the semantic versioning
17621787
// compatibility.

pgconn/pgconn_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2319,6 +2319,9 @@ func TestHijackAndConstruct(t *testing.T) {
23192319
origConn, err := pgconn.Connect(ctx, os.Getenv("PGX_TEST_DATABASE"))
23202320
require.NoError(t, err)
23212321

2322+
err = origConn.SyncConn(ctx)
2323+
require.NoError(t, err)
2324+
23222325
hc, err := origConn.Hijack()
23232326
require.NoError(t, err)
23242327

pgproto3/frontend.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,3 +361,7 @@ func (f *Frontend) findAuthenticationMessageType(src []byte) (BackendMessage, er
361361
func (f *Frontend) GetAuthType() uint32 {
362362
return f.authType
363363
}
364+
365+
func (f *Frontend) ReadBufferLen() int {
366+
return f.cr.wp - f.cr.rp
367+
}

0 commit comments

Comments
 (0)