Skip to content

Commit 0c978f0

Browse files
authored
fix: make COPY TO STDOUT robust to errors, again (#331)
1 parent 481a550 commit 0c978f0

File tree

4 files changed

+37
-28
lines changed

4 files changed

+37
-28
lines changed

pgserver/arrowwriter.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package pgserver
33
import (
44
"os"
55
"strings"
6+
"sync/atomic"
67

78
"github.com/apache/arrow-go/v18/arrow/ipc"
89
"github.com/apecloud/myduckserver/adapter"
@@ -62,24 +63,31 @@ func NewArrowWriter(
6263
}, nil
6364
}
6465

65-
func (dw *ArrowWriter) Start() (string, chan CopyToResult, error) {
66+
func (dw *ArrowWriter) Start(globalErr *atomic.Pointer[error]) (string, chan CopyToResult, error) {
6667
// Execute the statement in a separate goroutine.
6768
ch := make(chan CopyToResult, 1)
6869
go func() {
69-
defer os.Remove(dw.pipePath)
7070
defer close(ch)
7171

7272
dw.ctx.GetLogger().Tracef("Executing statement via Arrow interface: %s", dw.duckSQL)
7373
conn, err := adapter.GetConn(dw.ctx)
7474
if err != nil {
75+
globalErr.Store(&err)
7576
ch <- CopyToResult{Err: err}
7677
return
7778
}
7879

80+
// If there is a global error, return immediately.
81+
if e := globalErr.Load(); e != nil {
82+
ch <- CopyToResult{Err: *e}
83+
return
84+
}
85+
7986
// Open the pipe for writing.
8087
// This operation will block until the reader opens the pipe for reading.
8188
pipe, err := os.OpenFile(dw.pipePath, os.O_WRONLY, os.ModeNamedPipe)
8289
if err != nil {
90+
globalErr.Store(&err)
8391
ch <- CopyToResult{Err: err}
8492
return
8593
}
@@ -114,6 +122,7 @@ func (dw *ArrowWriter) Start() (string, chan CopyToResult, error) {
114122
}
115123
return recordReader.Err()
116124
}); err != nil {
125+
globalErr.Store(&err)
117126
ch <- CopyToResult{Err: err}
118127
return
119128
}

pgserver/connection_handler.go

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,13 +1414,13 @@ func (h *ConnectionHandler) handleCopyToStdout(query ConvertedStatement, copyTo
14141414
}
14151415
defer writer.Close()
14161416

1417-
pipePath, ch, err := writer.Start()
1417+
var globalErr atomic.Pointer[error]
1418+
pipePath, ch, err := writer.Start(&globalErr)
14181419
if err != nil {
14191420
return err
14201421
}
14211422

14221423
done := make(chan struct{})
1423-
var globalErr atomic.Value
14241424
var blocked atomic.Bool
14251425
blocked.Store(true)
14261426
go func() {
@@ -1431,7 +1431,8 @@ func (h *ConnectionHandler) handleCopyToStdout(query ConvertedStatement, copyTo
14311431
pipe, err := os.OpenFile(pipePath, os.O_RDONLY, os.ModeNamedPipe)
14321432
blocked.Store(false)
14331433
if err != nil {
1434-
globalErr.Store(fmt.Errorf("failed to open pipe for reading: %w", err))
1434+
err = fmt.Errorf("failed to open pipe for reading: %w", err)
1435+
globalErr.Store(&err)
14351436
cancel()
14361437
return
14371438
}
@@ -1465,39 +1466,39 @@ func (h *ConnectionHandler) handleCopyToStdout(query ConvertedStatement, copyTo
14651466

14661467
switch format {
14671468
case tree.CopyFormatText:
1468-
flag := true
1469+
responsed := false
14691470
reader := bufio.NewReader(pipe)
14701471
for {
14711472
line, err := reader.ReadSlice('\n')
14721473
if err != nil {
14731474
if err == io.EOF {
14741475
break
14751476
}
1476-
globalErr.Store(err)
1477+
globalErr.Store(&err)
14771478
cancel()
14781479
return
14791480
}
1480-
if flag {
1481-
flag = false
1481+
if !responsed {
1482+
responsed = true
14821483
count := bytes.Count(line, []byte{'\t'})
14831484
err := sendCopyOutResponse(count + 1)
14841485
if err != nil {
1485-
globalErr.Store(err)
1486+
globalErr.Store(&err)
14861487
cancel()
14871488
return
14881489
}
14891490
}
14901491
err = sendCopyData(line)
14911492
if err != nil {
1492-
globalErr.Store(err)
1493+
globalErr.Store(&err)
14931494
cancel()
14941495
return
14951496
}
14961497
}
14971498
default:
14981499
err := sendCopyOutResponse(1)
14991500
if err != nil {
1500-
globalErr.Store(err)
1501+
globalErr.Store(&err)
15011502
cancel()
15021503
return
15031504
}
@@ -1509,14 +1510,14 @@ func (h *ConnectionHandler) handleCopyToStdout(query ConvertedStatement, copyTo
15091510
if err == io.EOF {
15101511
break
15111512
}
1512-
globalErr.Store(err)
1513+
globalErr.Store(&err)
15131514
cancel()
15141515
return
15151516
}
15161517
if n > 0 {
15171518
err := sendCopyData(buf[:n])
15181519
if err != nil {
1519-
globalErr.Store(err)
1520+
globalErr.Store(&err)
15201521
cancel()
15211522
return
15221523
}
@@ -1528,30 +1529,29 @@ func (h *ConnectionHandler) handleCopyToStdout(query ConvertedStatement, copyTo
15281529
select {
15291530
case <-ctx.Done(): // Context is canceled
15301531
<-done
1531-
err, _ := globalErr.Load().(error)
1532-
return errors.Join(ctx.Err(), err)
1532+
if errPtr := globalErr.Load(); errPtr != nil {
1533+
return errors.Join(ctx.Err(), err)
1534+
}
1535+
return ctx.Err()
15331536
case result := <-ch:
15341537
if blocked.Load() {
15351538
// If the pipe is still opened for reading but the writer has exited,
15361539
// then we need to open the pipe for writing again to unblock the reader.
1537-
globalErr.Store(errors.Join(
1538-
fmt.Errorf("pipe is opened for reading but the writer has exited"),
1539-
result.Err,
1540-
))
15411540
pipe, _ := os.OpenFile(pipePath, os.O_WRONLY, os.ModeNamedPipe)
1541+
<-done
15421542
if pipe != nil {
15431543
pipe.Close()
15441544
}
1545+
} else {
1546+
<-done
15451547
}
15461548

1547-
<-done
1548-
15491549
if result.Err != nil {
15501550
return fmt.Errorf("failed to copy data: %w", result.Err)
15511551
}
15521552

1553-
if err, ok := globalErr.Load().(error); ok {
1554-
return err
1553+
if errPtr := globalErr.Load(); errPtr != nil {
1554+
return *errPtr
15551555
}
15561556

15571557
// After data is sent and the producer side is finished without errors, send CopyDone

pgserver/datawriter.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"strings"
7+
"sync/atomic"
78

89
"github.com/apecloud/myduckserver/adapter"
910
"github.com/apecloud/myduckserver/backend"
@@ -13,7 +14,7 @@ import (
1314
)
1415

1516
type DataWriter interface {
16-
Start() (string, chan CopyToResult, error)
17+
Start(globalErr *atomic.Pointer[error]) (string, chan CopyToResult, error)
1718
Close()
1819
}
1920

@@ -145,18 +146,18 @@ func NewDuckDataWriter(
145146
}, nil
146147
}
147148

148-
func (dw *DuckDataWriter) Start() (string, chan CopyToResult, error) {
149+
func (dw *DuckDataWriter) Start(globalErr *atomic.Pointer[error]) (string, chan CopyToResult, error) {
149150
// Execute the COPY TO statement in a separate goroutine.
150151
ch := make(chan CopyToResult, 1)
151152
go func() {
152-
defer os.Remove(dw.pipePath)
153153
defer close(ch)
154154

155155
dw.ctx.GetLogger().Tracef("Executing COPY TO statement: %s", dw.duckSQL)
156156

157157
// This operation will block until the reader opens the pipe for reading.
158158
result, err := adapter.ExecCatalog(dw.ctx, dw.duckSQL)
159159
if err != nil {
160+
globalErr.Store(&err)
160161
ch <- CopyToResult{Err: err}
161162
return
162163
}

test/bats/postgres/copy_tests.bats

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ EOF
133133
}
134134

135135
@test "copy error handling" {
136-
skip
137136
# Test copying from non-existent schema
138137
run psql_exec "\copy nonexistent_schema.t TO STDOUT;"
139138
[ "$status" -ne 0 ]

0 commit comments

Comments
 (0)