Repository navigation
Expand file tree
/
Copy patharrow_stream.go
More file actions
428 lines (382 loc) · 11.8 KB
/
Copy patharrow_stream.go
File metadata and controls
428 lines (382 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
package gosnowflake
import (
"bufio"
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"fmt"
"io"
"maps"
"net/http"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/apache/arrow-go/v18/arrow/ipc"
"github.com/snowflakedb/gosnowflake/v2/internal/query"
)
// ArrowStreamLoader is a convenience interface for downloading
// Snowflake results via multiple Arrow Record Batch streams.
//
// Some queries from Snowflake do not return Arrow data regardless
// of the settings, such as "SHOW WAREHOUSES" or SQL stored procedures
// using CALL with RETURNS TABLE(). In these cases the server returns
// JSON even though the client requested Arrow. Small JSON results are
// accessible via JSONData(); column metadata is available via RowTypes().
//
// To determine the actual response format, use QueryResultFormatProvider.
type ArrowStreamLoader interface {
GetBatches() ([]ArrowStreamBatch, error)
NextResultSet(ctx context.Context) error
TotalRows() int64
RowTypes() []query.ExecResponseRowType
Location() *time.Location
JSONData() [][]*string
}
// QueryResultFormatProvider is an optional interface that an
// ArrowStreamLoader may implement to expose the server-reported result
// format. The returned value is typically "arrow" or "json".
// Callers should check the format before interpreting data from
// GetBatches, as the batch content may be Arrow IPC or JSON depending
// on the statement type.
//
// if p, ok := loader.(QueryResultFormatProvider); ok {
// fmt.Println(p.QueryResultFormat())
// }
type QueryResultFormatProvider interface {
QueryResultFormat() string
}
// ArrowStreamBatch is a type describing a potentially yet-to-be-downloaded
// chunk of query result data. The content format depends on the current
// QueryResultFormat: Arrow IPC record batches (use ipc.NewReader) when
// the format is "arrow", or JSON (row fragments) when it is "json".
type ArrowStreamBatch struct {
idx int
numrows int64
scd *snowflakeArrowStreamChunkDownloader
Loc *time.Location
rr io.ReadCloser
inlineData []byte // non-nil for inline (RowSetBase64) batches
}
// NumRows returns the total number of rows that the metadata stated should
// be in this stream of record batches.
func (asb *ArrowStreamBatch) NumRows() int64 { return asb.numrows }
// Reset closes any existing stream and clears the cached reader, allowing
// GetStream to re-download the chunk on the next call. This enables callers
// to retry after a mid-stream failure (e.g. TCP RST) without re-executing
// the entire query.
//
// For inline batches (those produced from RowSetBase64 in the initial
// query response), there is no remote chunk to re-download. In that
// case Reset rewinds the reader by re-wrapping the cached inline bytes
// so the batch can be read again from the beginning. This is done
// even when the underlying Close returns an error, so the batch is
// always left in a usable state for retry; the close error is still
// returned to the caller.
func (asb *ArrowStreamBatch) Reset() error {
var closeErr error
if asb.rr != nil {
closeErr = asb.rr.Close()
asb.rr = nil
}
if asb.inlineData != nil {
asb.rr = io.NopCloser(bytes.NewReader(asb.inlineData))
}
return closeErr
}
// GetStream downloads the chunk (if not already cached) and returns a
// stream of bytes. The content may be Arrow IPC or JSON (row fragments)
// depending on the current QueryResultFormat. Close should be called
// on the returned stream when done to ensure no leaked memory.
//
// If a previous stream failed mid-read, call Reset() first to clear the
// cached reader and allow re-download.
func (asb *ArrowStreamBatch) GetStream(ctx context.Context) (io.ReadCloser, error) {
if asb.rr == nil {
if err := asb.downloadChunkStreamHelper(ctx); err != nil {
return nil, err
}
}
return newCancelableStream(ctx, asb.rr), nil
}
// streamWrapReader wraps an io.Reader so that Close closes the underlying body.
type streamWrapReader struct {
io.Reader
wrapped io.ReadCloser
}
func (w *streamWrapReader) Close() error {
if cl, ok := w.Reader.(io.ReadCloser); ok {
if err := cl.Close(); err != nil {
return err
}
}
return w.wrapped.Close()
}
// interrupt closes the raw response body directly. This bypasses gzip.Reader
// cleanup because the gzip layer may still be blocked waiting for more bytes,
// while closing the transport body is what unblocks the in-flight read.
func (w *streamWrapReader) interrupt() error {
return w.wrapped.Close()
}
// interruptibleReader lets cancelableStream bypass wrapper cleanup and close
// the raw transport body that actually unblocks a stalled read.
type interruptibleReader interface {
interrupt() error
}
// cancelableStream watches the stream context so cancellation can interrupt a
// stalled read, normalizes terminal transport errors caused by that interrupt
// to ctx.Err(), and still preserves true successful completion and EOF.
type cancelableStream struct {
ctx context.Context
inner io.ReadCloser
interrupt func() error
done chan struct{}
stopOnce sync.Once
interruptOnce sync.Once
completed atomic.Bool
}
func newCancelableStream(ctx context.Context, inner io.ReadCloser) io.ReadCloser {
out := &cancelableStream{
ctx: ctx,
inner: inner,
interrupt: inner.Close,
done: make(chan struct{}),
}
if interruptible, ok := inner.(interruptibleReader); ok {
out.interrupt = interruptible.interrupt
}
if ctx.Done() == nil {
return out
}
go func() {
select {
case <-ctx.Done():
if out.watchingStopped() || out.completed.Load() {
return
}
out.interruptOnce.Do(func() {
_ = out.interrupt()
})
case <-out.done:
}
}()
return out
}
func (r *cancelableStream) stopWatching() {
r.stopOnce.Do(func() {
close(r.done)
})
}
func (r *cancelableStream) watchingStopped() bool {
select {
case <-r.done:
return true
default:
return false
}
}
func (r *cancelableStream) Read(p []byte) (int, error) {
n, err := r.inner.Read(p)
if err == nil {
return n, nil
}
defer r.stopWatching()
// Preserve the successful final read where io.Reader returns payload and EOF
// together. Only terminal reads with no delivered bytes are candidates for
// cancellation rewriting.
if err == io.EOF && n > 0 {
r.completed.Store(true)
return n, err
}
if normalized := r.canceledReadErr(); normalized != nil {
return n, normalized
}
if err == io.EOF {
r.completed.Store(true)
}
return n, err
}
func (r *cancelableStream) canceledReadErr() error {
ctxErr := r.ctx.Err()
if ctxErr == nil || r.completed.Load() {
return nil
}
// A canceled context wins over terminal transport errors even if the watcher
// goroutine has not yet unblocked the read before the terminal error arrives.
return ctxErr
}
func (r *cancelableStream) Close() error {
r.stopWatching()
return r.inner.Close()
}
func (asb *ArrowStreamBatch) downloadChunkStreamHelper(ctx context.Context) error {
headers := make(map[string]string)
if len(asb.scd.ChunkHeader) > 0 {
maps.Copy(headers, asb.scd.ChunkHeader)
} else {
headers[headerSseCAlgorithm] = headerSseCAes
headers[headerSseCKey] = asb.scd.Qrmk
}
resp, err := asb.scd.FuncGet(ctx, asb.scd.sc, asb.scd.ChunkMetas[asb.idx].URL, headers, asb.scd.sc.rest.RequestTimeout)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
defer func() {
_ = resp.Body.Close()
}()
b, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
_ = b
return &SnowflakeError{
Number: ErrFailedToGetChunk,
SQLState: SQLStateConnectionFailure,
Message: fmt.Sprintf("failed to get chunk. idx: %v", asb.idx),
MessageArgs: []any{asb.idx},
}
}
defer func() {
if asb.rr == nil {
_ = resp.Body.Close()
}
}()
bufStream := bufio.NewReader(resp.Body)
gzipMagic, err := bufStream.Peek(2)
if err != nil {
return err
}
if gzipMagic[0] == 0x1f && gzipMagic[1] == 0x8b {
bufStream0, err := gzip.NewReader(bufStream)
if err != nil {
return err
}
asb.rr = &streamWrapReader{Reader: bufStream0, wrapped: resp.Body}
} else {
asb.rr = &streamWrapReader{Reader: bufStream, wrapped: resp.Body}
}
return nil
}
type snowflakeArrowStreamChunkDownloader struct {
sc *snowflakeConn
ChunkMetas []query.ExecResponseChunk
Total int64
Qrmk string
ChunkHeader map[string]string
FuncGet func(context.Context, *snowflakeConn, string, map[string]string, time.Duration) (*http.Response, error)
RowSet rowSetType
resultIDs []string
queryResultFormat string
}
func (scd *snowflakeArrowStreamChunkDownloader) Location() *time.Location {
if scd.sc != nil {
return getCurrentLocation(&scd.sc.syncParams)
}
return nil
}
func (scd *snowflakeArrowStreamChunkDownloader) TotalRows() int64 { return scd.Total }
func (scd *snowflakeArrowStreamChunkDownloader) RowTypes() []query.ExecResponseRowType {
return scd.RowSet.RowType
}
func (scd *snowflakeArrowStreamChunkDownloader) JSONData() [][]*string {
return scd.RowSet.JSON
}
// QueryResultFormat returns the server-reported result format for the
// current result set (typically "arrow" or "json"). Callers should
// check this before interpreting batch streams from GetBatches, as
// the content may be Arrow IPC or JSON depending on the statement type.
// The value is updated on each NextResultSet call.
func (scd *snowflakeArrowStreamChunkDownloader) QueryResultFormat() string {
return scd.queryResultFormat
}
func (scd *snowflakeArrowStreamChunkDownloader) maybeFirstBatch() ([]byte, error) {
if scd.RowSet.RowSetBase64 == "" {
return nil, nil
}
rowSetBytes, err := base64.StdEncoding.DecodeString(scd.RowSet.RowSetBase64)
if err != nil {
logger.Warnf("skipping first batch as it is not a valid base64 response. %v", err)
return nil, err
}
rr, err := ipc.NewReader(bytes.NewReader(rowSetBytes))
if err != nil {
logger.Warnf("skipping first batch as it is not a valid IPC stream. %v", err)
return nil, err
}
rr.Release()
return rowSetBytes, nil
}
func (scd *snowflakeArrowStreamChunkDownloader) GetBatches() (out []ArrowStreamBatch, err error) {
chunkMetaLen := len(scd.ChunkMetas)
loc := scd.Location()
out = make([]ArrowStreamBatch, chunkMetaLen, chunkMetaLen+1)
toFill := out
rowSetBytes, err := scd.maybeFirstBatch()
if err != nil {
return nil, err
}
if len(rowSetBytes) > 0 {
out = out[:chunkMetaLen+1]
out[0] = ArrowStreamBatch{
scd: scd,
Loc: loc,
rr: io.NopCloser(bytes.NewReader(rowSetBytes)),
inlineData: rowSetBytes,
}
toFill = out[1:]
}
var totalCounted int64
for i := range toFill {
toFill[i] = ArrowStreamBatch{
idx: i,
numrows: int64(scd.ChunkMetas[i].RowCount),
Loc: loc,
scd: scd,
}
totalCounted += int64(scd.ChunkMetas[i].RowCount)
}
if len(rowSetBytes) > 0 {
out[0].numrows = scd.Total - totalCounted
}
return
}
func (scd *snowflakeArrowStreamChunkDownloader) NextResultSet(ctx context.Context) error {
if !scd.hasNextResultSet() {
return io.EOF
}
resultID := scd.resultIDs[0]
scd.resultIDs = scd.resultIDs[1:]
resultPath := fmt.Sprintf(urlQueriesResultFmt, resultID)
resp, err := scd.sc.getQueryResultResp(ctx, resultPath)
if err != nil {
return err
}
if !resp.Success {
code, err := strconv.Atoi(resp.Code)
if err != nil {
logger.WithContext(ctx).Errorf("error while parsing code: %v", err)
}
return exceptionTelemetry(&SnowflakeError{
Number: code,
SQLState: resp.Data.SQLState,
Message: resp.Message,
QueryID: resp.Data.QueryID,
}, scd.sc)
}
scd.ChunkMetas = resp.Data.Chunks
scd.Total = resp.Data.Total
scd.Qrmk = resp.Data.Qrmk
scd.ChunkHeader = resp.Data.ChunkHeaders
scd.queryResultFormat = resp.Data.QueryResultFormat
scd.RowSet = rowSetType{
RowType: resp.Data.RowType,
JSON: resp.Data.RowSet,
RowSetBase64: resp.Data.RowSetBase64,
}
return nil
}
func (scd *snowflakeArrowStreamChunkDownloader) hasNextResultSet() bool {
return len(scd.resultIDs) > 0
}