rubydb 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile.lock +1 -1
- data/README.md +296 -227
- data/Rakefile +6 -1
- data/accelerator/bin/SHA256SUMS +6 -0
- data/accelerator/bin/rubydb-accelerator-darwin-amd64 +0 -0
- data/accelerator/bin/rubydb-accelerator-darwin-arm64 +0 -0
- data/accelerator/bin/rubydb-accelerator-linux-amd64 +0 -0
- data/accelerator/bin/rubydb-accelerator-linux-arm64 +0 -0
- data/accelerator/bin/rubydb-accelerator-windows-amd64.exe +0 -0
- data/accelerator/bin/rubydb-accelerator-windows-arm64.exe +0 -0
- data/accelerator/cmd/rubydb-accelerator/main.go +11 -0
- data/accelerator/go.mod +3 -0
- data/accelerator/internal/execution/aggregate.go +94 -0
- data/accelerator/internal/execution/distinct.go +22 -0
- data/accelerator/internal/execution/filter.go +73 -0
- data/accelerator/internal/execution/join.go +79 -0
- data/accelerator/internal/execution/operators.go +167 -0
- data/accelerator/internal/execution/scan.go +20 -0
- data/accelerator/internal/execution/sort.go +62 -0
- data/accelerator/internal/execution/types.go +136 -0
- data/accelerator/internal/execution/value.go +67 -0
- data/accelerator/internal/memory/arena.go +47 -0
- data/accelerator/internal/memory/reuse.go +22 -0
- data/accelerator/internal/metrics/registry.go +67 -0
- data/accelerator/internal/parallel/bounded_queue.go +56 -0
- data/accelerator/internal/parallel/scheduler.go +47 -0
- data/accelerator/internal/parallel/worker_pool.go +53 -0
- data/accelerator/internal/protocol/cancellation.go +48 -0
- data/accelerator/internal/protocol/columnar.go +263 -0
- data/accelerator/internal/protocol/frame.go +187 -0
- data/accelerator/internal/runtime/worker.go +521 -0
- data/accelerator/internal/storage/page_reader.go +81 -0
- data/accelerator/internal/storage/snapshot_scan.go +539 -0
- data/accelerator/internal/wal/checksum.go +13 -0
- data/accelerator/internal/wal/compression.go +41 -0
- data/accelerator/internal/wal/group_commit.go +24 -0
- data/accelerator/internal/wal/record_encoder.go +40 -0
- data/adapters/activerecord/README.md +8 -3
- data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +50 -42
- data/adapters/activerecord/rubydb-activerecord.gemspec +1 -1
- data/docs/README.md +3 -1
- data/docs/architecture/go-accelerator.md +179 -0
- data/docs/cli.md +24 -0
- data/docs/contributing/benchmarking.md +16 -0
- data/docs/developer/local-development.md +32 -0
- data/docs/release.md +2 -2
- data/lessons/02-local-development.md +2 -2
- data/lessons/04-rails-complex-apps.md +2 -2
- data/lessons/05-rubydb-production-server.md +2 -2
- data/lessons/07-hybrid-microservices.md +175 -90
- data/lessons/10-release-readiness.md +184 -117
- data/lessons/11-community-adapter.md +323 -0
- data/lessons/12-rails-ecommerce-pressure.md +263 -0
- data/lib/rubydb/accelerator/client.rb +451 -0
- data/lib/rubydb/accelerator/error.rb +22 -0
- data/lib/rubydb/accelerator/manager.rb +606 -0
- data/lib/rubydb/accelerator.rb +13 -0
- data/lib/rubydb/cli/application.rb +6 -1
- data/lib/rubydb/cli/commands/accelerator.rb +72 -0
- data/lib/rubydb/cli/commands/doctor.rb +3 -0
- data/lib/rubydb/client/client.rb +7 -0
- data/lib/rubydb/client/connection.rb +15 -0
- data/lib/rubydb/client/result.rb +5 -1
- data/lib/rubydb/configuration/defaults.rb +12 -0
- data/lib/rubydb/configuration/validation.rb +8 -1
- data/lib/rubydb/execution/accelerator_dispatch.rb +30 -0
- data/lib/rubydb/execution/cost_model.rb +72 -0
- data/lib/rubydb/execution/executor.rb +373 -11
- data/lib/rubydb/execution/operator_selection.rb +57 -0
- data/lib/rubydb/execution/physical_plan.rb +47 -0
- data/lib/rubydb/execution/planner.rb +12 -46
- data/lib/rubydb/execution/sort_executor.rb +22 -8
- data/lib/rubydb/indexes/btree.rb +31 -2
- data/lib/rubydb/rubydb.rb +7 -1
- data/lib/rubydb/server/session.rb +45 -0
- data/lib/rubydb/storage/engine.rb +74 -12
- data/lib/rubydb/storage/snapshot_reader.rb +167 -0
- data/lib/rubydb/version.rb +1 -1
- data/lib/rubydb/wal/archive.rb +17 -0
- data/lib/rubydb/wal/wal.rb +1 -0
- data/rubydb.gemspec +12 -2
- data/scripts/build_accelerator +49 -0
- data/scripts/release +34 -4
- data/scripts/replication_failover_drill +2 -2
- metadata +49 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
package protocol
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"bytes"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"errors"
|
|
8
|
+
"fmt"
|
|
9
|
+
"io"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
const (
|
|
13
|
+
Version = 1
|
|
14
|
+
BinaryVersion = 1
|
|
15
|
+
MaxFrameSize = 16 * 1024 * 1024
|
|
16
|
+
|
|
17
|
+
BinaryRequestKind byte = 1
|
|
18
|
+
BinaryResponseKind byte = 2
|
|
19
|
+
BinaryRowsType byte = 1
|
|
20
|
+
BinaryJoinType byte = 2
|
|
21
|
+
BinarySnapshotScanType byte = 3
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
var BinaryMagic = []byte("RDBB")
|
|
25
|
+
|
|
26
|
+
var ErrFrameTooLarge = errors.New("frame too large")
|
|
27
|
+
|
|
28
|
+
type BinaryFrame struct {
|
|
29
|
+
Kind byte
|
|
30
|
+
TypeID byte
|
|
31
|
+
Status byte
|
|
32
|
+
ID string
|
|
33
|
+
Payload []byte
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func ReadMessage(reader *bufio.Reader) (*BinaryFrame, []byte, error) {
|
|
37
|
+
prefix, err := reader.Peek(len(BinaryMagic))
|
|
38
|
+
if err != nil {
|
|
39
|
+
return nil, nil, err
|
|
40
|
+
}
|
|
41
|
+
if bytes.Equal(prefix, BinaryMagic) {
|
|
42
|
+
frame, err := ReadBinaryFrame(reader)
|
|
43
|
+
return frame, nil, err
|
|
44
|
+
}
|
|
45
|
+
frame, err := ReadFrame(reader)
|
|
46
|
+
return nil, frame, err
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func ReadBinaryFrame(reader *bufio.Reader) (*BinaryFrame, error) {
|
|
50
|
+
header := make([]byte, 14)
|
|
51
|
+
if _, err := io.ReadFull(reader, header); err != nil {
|
|
52
|
+
return nil, err
|
|
53
|
+
}
|
|
54
|
+
if !bytes.Equal(header[:4], BinaryMagic) || header[4] != BinaryVersion {
|
|
55
|
+
return nil, errors.New("invalid binary frame")
|
|
56
|
+
}
|
|
57
|
+
idLength := int(binaryLittleEndian16(header[8:10]))
|
|
58
|
+
payloadLength := int(binaryLittleEndian32(header[10:14]))
|
|
59
|
+
if payloadLength > MaxFrameSize || idLength > 1024 || payloadLength+idLength > MaxFrameSize {
|
|
60
|
+
return nil, ErrFrameTooLarge
|
|
61
|
+
}
|
|
62
|
+
body := make([]byte, idLength+payloadLength)
|
|
63
|
+
if _, err := io.ReadFull(reader, body); err != nil {
|
|
64
|
+
return nil, err
|
|
65
|
+
}
|
|
66
|
+
return &BinaryFrame{
|
|
67
|
+
Kind: header[5],
|
|
68
|
+
TypeID: header[6],
|
|
69
|
+
Status: header[7],
|
|
70
|
+
ID: string(body[:idLength]),
|
|
71
|
+
Payload: body[idLength:],
|
|
72
|
+
}, nil
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func WriteBinaryResponse(writer *bufio.Writer, requestFrame BinaryFrame, payload []byte, message, code string) error {
|
|
76
|
+
status := byte(0)
|
|
77
|
+
if message != "" {
|
|
78
|
+
status = 1
|
|
79
|
+
payload = MustJSON(map[string]string{"error": message, "code": code})
|
|
80
|
+
}
|
|
81
|
+
if len(requestFrame.ID) > 1024 || len(payload)+len(requestFrame.ID) > MaxFrameSize {
|
|
82
|
+
return ErrFrameTooLarge
|
|
83
|
+
}
|
|
84
|
+
header := make([]byte, 14)
|
|
85
|
+
copy(header[:4], BinaryMagic)
|
|
86
|
+
header[4] = BinaryVersion
|
|
87
|
+
header[5] = BinaryResponseKind
|
|
88
|
+
header[6] = requestFrame.TypeID
|
|
89
|
+
header[7] = status
|
|
90
|
+
putBinaryLittleEndian16(header[8:10], uint16(len(requestFrame.ID)))
|
|
91
|
+
putBinaryLittleEndian32(header[10:14], uint32(len(payload)))
|
|
92
|
+
if _, err := writer.Write(header); err != nil {
|
|
93
|
+
return err
|
|
94
|
+
}
|
|
95
|
+
if _, err := writer.WriteString(requestFrame.ID); err != nil {
|
|
96
|
+
return err
|
|
97
|
+
}
|
|
98
|
+
if _, err := writer.Write(payload); err != nil {
|
|
99
|
+
return err
|
|
100
|
+
}
|
|
101
|
+
return writer.Flush()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func ReadFrame(reader *bufio.Reader) ([]byte, error) {
|
|
105
|
+
frame := make([]byte, 0, 1024)
|
|
106
|
+
for {
|
|
107
|
+
chunk, err := reader.ReadSlice('\n')
|
|
108
|
+
if len(frame)+len(chunk) > MaxFrameSize {
|
|
109
|
+
return nil, ErrFrameTooLarge
|
|
110
|
+
}
|
|
111
|
+
frame = append(frame, chunk...)
|
|
112
|
+
if err == bufio.ErrBufferFull {
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
if err != nil {
|
|
116
|
+
return nil, err
|
|
117
|
+
}
|
|
118
|
+
return frame, nil
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func WriteJSONResponse(writer *bufio.Writer, value interface{}) error {
|
|
123
|
+
data, err := json.Marshal(value)
|
|
124
|
+
if err != nil {
|
|
125
|
+
return err
|
|
126
|
+
}
|
|
127
|
+
if len(data)+1 > MaxFrameSize {
|
|
128
|
+
return fmt.Errorf("response exceeds maximum frame size")
|
|
129
|
+
}
|
|
130
|
+
if _, err = writer.Write(append(data, '\n')); err != nil {
|
|
131
|
+
return err
|
|
132
|
+
}
|
|
133
|
+
return writer.Flush()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
func MustJSON(value interface{}) []byte {
|
|
137
|
+
data, _ := json.Marshal(value)
|
|
138
|
+
return data
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
type Reader struct {
|
|
142
|
+
data []byte
|
|
143
|
+
offset int
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
func NewReader(data []byte) *Reader { return &Reader{data: data} }
|
|
147
|
+
|
|
148
|
+
func (reader *Reader) Remaining() int { return len(reader.data) - reader.offset }
|
|
149
|
+
|
|
150
|
+
func (reader *Reader) Bytes() []byte { return reader.data[reader.offset:] }
|
|
151
|
+
|
|
152
|
+
func (reader *Reader) Take(length int) ([]byte, error) {
|
|
153
|
+
if length < 0 || reader.Remaining() < length {
|
|
154
|
+
return nil, errors.New("binary value is truncated")
|
|
155
|
+
}
|
|
156
|
+
value := reader.data[reader.offset : reader.offset+length]
|
|
157
|
+
reader.offset += length
|
|
158
|
+
return value, nil
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func (reader *Reader) Uint32() (uint32, error) {
|
|
162
|
+
value, err := reader.Take(4)
|
|
163
|
+
if err != nil {
|
|
164
|
+
return 0, err
|
|
165
|
+
}
|
|
166
|
+
return binaryLittleEndian32(value), nil
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func binaryLittleEndian16(data []byte) uint16 {
|
|
170
|
+
return uint16(data[0]) | uint16(data[1])<<8
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func binaryLittleEndian32(data []byte) uint32 {
|
|
174
|
+
return uint32(data[0]) | uint32(data[1])<<8 | uint32(data[2])<<16 | uint32(data[3])<<24
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
func putBinaryLittleEndian16(data []byte, value uint16) {
|
|
178
|
+
data[0] = byte(value)
|
|
179
|
+
data[1] = byte(value >> 8)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
func putBinaryLittleEndian32(data []byte, value uint32) {
|
|
183
|
+
data[0] = byte(value)
|
|
184
|
+
data[1] = byte(value >> 8)
|
|
185
|
+
data[2] = byte(value >> 16)
|
|
186
|
+
data[3] = byte(value >> 24)
|
|
187
|
+
}
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"bytes"
|
|
6
|
+
"compress/gzip"
|
|
7
|
+
"context"
|
|
8
|
+
"encoding/base64"
|
|
9
|
+
"encoding/json"
|
|
10
|
+
"errors"
|
|
11
|
+
"io"
|
|
12
|
+
"runtime"
|
|
13
|
+
"sync"
|
|
14
|
+
"time"
|
|
15
|
+
|
|
16
|
+
"github.com/aldanedev-create/rubydb/accelerator/internal/execution"
|
|
17
|
+
"github.com/aldanedev-create/rubydb/accelerator/internal/metrics"
|
|
18
|
+
"github.com/aldanedev-create/rubydb/accelerator/internal/protocol"
|
|
19
|
+
"github.com/aldanedev-create/rubydb/accelerator/internal/storage"
|
|
20
|
+
"github.com/aldanedev-create/rubydb/accelerator/internal/wal"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
type request struct {
|
|
24
|
+
ID string `json:"id"`
|
|
25
|
+
Type string `json:"type"`
|
|
26
|
+
Payload json.RawMessage `json:"payload"`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type response struct {
|
|
30
|
+
ID string `json:"id"`
|
|
31
|
+
Type string `json:"type"`
|
|
32
|
+
Success bool `json:"success"`
|
|
33
|
+
More bool `json:"more,omitempty"`
|
|
34
|
+
Payload interface{} `json:"payload,omitempty"`
|
|
35
|
+
Error string `json:"error,omitempty"`
|
|
36
|
+
Code string `json:"code,omitempty"`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
var operationMetrics = metrics.NewRegistry()
|
|
40
|
+
|
|
41
|
+
func Run(input io.Reader, output io.Writer) {
|
|
42
|
+
reader := bufio.NewReaderSize(input, 64*1024)
|
|
43
|
+
writer := bufio.NewWriterSize(output, 64*1024)
|
|
44
|
+
var writeMu sync.Mutex
|
|
45
|
+
registry := protocol.NewCancellationRegistry()
|
|
46
|
+
workerLimit := runtime.GOMAXPROCS(0) * 2
|
|
47
|
+
// Keep a small amount of parallelism available even on development
|
|
48
|
+
// machines where Go reports only one or two logical processors. The Ruby
|
|
49
|
+
// client multiplexes requests over one worker, so rejecting every request
|
|
50
|
+
// above GOMAXPROCS*2 would turn normal connection-pool bursts into false
|
|
51
|
+
// accelerator failures. The limit is still bounded and remains a safety
|
|
52
|
+
// valve for large requests.
|
|
53
|
+
if workerLimit < 8 {
|
|
54
|
+
workerLimit = 8
|
|
55
|
+
}
|
|
56
|
+
active := make(chan struct{}, workerLimit)
|
|
57
|
+
defer writer.Flush()
|
|
58
|
+
for {
|
|
59
|
+
binaryFrame, frame, err := protocol.ReadMessage(reader)
|
|
60
|
+
if err != nil {
|
|
61
|
+
if errors.Is(err, io.EOF) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
if errors.Is(err, protocol.ErrFrameTooLarge) {
|
|
65
|
+
_ = protocol.WriteJSONResponse(writer, response{Success: false, Type: "error", Error: "request exceeds maximum frame size", Code: "frame_too_large"})
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
_ = protocol.WriteJSONResponse(writer, response{Success: false, Type: "error", Error: "invalid request frame", Code: "invalid_frame"})
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
if binaryFrame != nil {
|
|
72
|
+
frame := *binaryFrame
|
|
73
|
+
select {
|
|
74
|
+
case active <- struct{}{}:
|
|
75
|
+
default:
|
|
76
|
+
writeMu.Lock()
|
|
77
|
+
_ = protocol.WriteBinaryResponse(writer, frame, nil, "accelerator worker capacity is exhausted", "resource_limit")
|
|
78
|
+
writeMu.Unlock()
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
ctx := registry.Begin(context.Background(), frame.ID)
|
|
82
|
+
go func() {
|
|
83
|
+
defer func() { <-active }()
|
|
84
|
+
defer registry.Finish(frame.ID)
|
|
85
|
+
payload, message, code := handleBinaryContext(ctx, frame)
|
|
86
|
+
writeMu.Lock()
|
|
87
|
+
defer writeMu.Unlock()
|
|
88
|
+
_ = protocol.WriteBinaryResponse(writer, frame, payload, message, code)
|
|
89
|
+
}()
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
var req request
|
|
93
|
+
decoder := json.NewDecoder(bytes.NewReader(bytes.TrimSpace(frame)))
|
|
94
|
+
decoder.UseNumber()
|
|
95
|
+
if err := decoder.Decode(&req); err != nil || req.ID == "" || req.Type == "" {
|
|
96
|
+
_ = protocol.WriteJSONResponse(writer, response{Success: false, Type: "error", Error: "invalid request frame", Code: "invalid_frame"})
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
if req.Type == "cancel" {
|
|
100
|
+
var payload struct {
|
|
101
|
+
TargetID string `json:"target_id"`
|
|
102
|
+
}
|
|
103
|
+
if err := json.Unmarshal(req.Payload, &payload); err != nil || payload.TargetID == "" {
|
|
104
|
+
writeMu.Lock()
|
|
105
|
+
_ = protocol.WriteJSONResponse(writer, response{ID: req.ID, Success: false, Type: "error", Error: "invalid cancellation payload", Code: "invalid_payload"})
|
|
106
|
+
writeMu.Unlock()
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
cancelled := registry.Cancel(payload.TargetID)
|
|
110
|
+
writeMu.Lock()
|
|
111
|
+
_ = protocol.WriteJSONResponse(writer, response{ID: req.ID, Type: "cancel_response", Success: true, Payload: map[string]interface{}{"target_id": payload.TargetID, "cancelled": cancelled}})
|
|
112
|
+
writeMu.Unlock()
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
if req.Type == "terminate" {
|
|
116
|
+
res := handleContext(context.Background(), req)
|
|
117
|
+
res.ID = req.ID
|
|
118
|
+
writeMu.Lock()
|
|
119
|
+
err := protocol.WriteJSONResponse(writer, res)
|
|
120
|
+
writeMu.Unlock()
|
|
121
|
+
if err != nil {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
select {
|
|
127
|
+
case active <- struct{}{}:
|
|
128
|
+
default:
|
|
129
|
+
writeMu.Lock()
|
|
130
|
+
_ = protocol.WriteJSONResponse(writer, response{ID: req.ID, Type: "error", Success: false, Error: "accelerator worker capacity is exhausted", Code: "resource_limit"})
|
|
131
|
+
writeMu.Unlock()
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
ctx := registry.Begin(context.Background(), req.ID)
|
|
135
|
+
go func(req request, ctx context.Context) {
|
|
136
|
+
defer func() { <-active }()
|
|
137
|
+
defer registry.Finish(req.ID)
|
|
138
|
+
started := time.Now()
|
|
139
|
+
if req.Type == "rows_pipeline_stream" {
|
|
140
|
+
streamRowsPipeline(ctx, req, writer, &writeMu)
|
|
141
|
+
operationMetrics.Observe(req.Type, time.Since(started))
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
res := handleContext(ctx, req)
|
|
145
|
+
operationMetrics.Observe(req.Type, time.Since(started))
|
|
146
|
+
res.ID = req.ID
|
|
147
|
+
writeMu.Lock()
|
|
148
|
+
defer writeMu.Unlock()
|
|
149
|
+
_ = protocol.WriteJSONResponse(writer, res)
|
|
150
|
+
}(req, ctx)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
func handle(req request) response {
|
|
155
|
+
return handleContext(context.Background(), req)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func handleContext(ctx context.Context, req request) response {
|
|
159
|
+
select {
|
|
160
|
+
case <-ctx.Done():
|
|
161
|
+
return failure(ctx.Err().Error(), "cancelled")
|
|
162
|
+
default:
|
|
163
|
+
}
|
|
164
|
+
switch req.Type {
|
|
165
|
+
case "handshake":
|
|
166
|
+
return response{Success: true, Type: "handshake_response", Payload: map[string]interface{}{
|
|
167
|
+
"protocol_version": protocolVersion,
|
|
168
|
+
"accelerator_version": "0.1.0",
|
|
169
|
+
"runtime": "go",
|
|
170
|
+
"capabilities": capabilities(),
|
|
171
|
+
}}
|
|
172
|
+
case "ping":
|
|
173
|
+
return response{Success: true, Type: "pong", Payload: map[string]interface{}{
|
|
174
|
+
"protocol_version": protocolVersion,
|
|
175
|
+
"capabilities": capabilities(),
|
|
176
|
+
}}
|
|
177
|
+
case "stats":
|
|
178
|
+
return response{Success: true, Type: "stats_response", Payload: operationMetrics.Snapshot()}
|
|
179
|
+
case "sha256":
|
|
180
|
+
return handleSHA256(req.Payload)
|
|
181
|
+
case "gzip":
|
|
182
|
+
return handleGzip(req.Payload, true)
|
|
183
|
+
case "gunzip":
|
|
184
|
+
return handleGzip(req.Payload, false)
|
|
185
|
+
case "rows_pipeline":
|
|
186
|
+
return handleRowsPipelineContext(ctx, req.Payload)
|
|
187
|
+
case "wal_batch":
|
|
188
|
+
return handleWALBatch(req.Payload)
|
|
189
|
+
case "terminate":
|
|
190
|
+
return response{Success: true, Type: "terminate_response"}
|
|
191
|
+
default:
|
|
192
|
+
return failure("unsupported accelerator operation", "unsupported_operation")
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
func capabilities() []string {
|
|
197
|
+
return []string{"sha256", "gzip", "wal_batch", "rows_pipeline", "rows_pipeline_stream", "rows_pipeline_binary", "hash_join", "merge_join", "snapshot_scan", "snapshot_index_scan", "columnar_batches", "projection", "distinct", "window_functions", "cancellation", "multiplexed_requests", "bounded_concurrency", "resource_limits", "metrics"}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
func handleBinary(frame protocol.BinaryFrame) ([]byte, string, string) {
|
|
201
|
+
return handleBinaryContext(context.Background(), frame)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
func handleBinaryContext(ctx context.Context, frame protocol.BinaryFrame) ([]byte, string, string) {
|
|
205
|
+
if frame.Kind != protocol.BinaryRequestKind || (frame.TypeID != protocol.BinaryRowsType && frame.TypeID != protocol.BinaryJoinType && frame.TypeID != protocol.BinarySnapshotScanType) {
|
|
206
|
+
return nil, "unsupported binary accelerator operation", "unsupported_operation"
|
|
207
|
+
}
|
|
208
|
+
if frame.TypeID == protocol.BinarySnapshotScanType {
|
|
209
|
+
var request storage.SnapshotScanRequest
|
|
210
|
+
decoder := json.NewDecoder(bytes.NewReader(frame.Payload))
|
|
211
|
+
decoder.UseNumber()
|
|
212
|
+
if err := decoder.Decode(&request); err != nil {
|
|
213
|
+
return nil, "snapshot scan request is invalid", "invalid_payload"
|
|
214
|
+
}
|
|
215
|
+
result, err := storage.ExecuteSnapshotScanContext(ctx, request)
|
|
216
|
+
if err != nil {
|
|
217
|
+
return nil, err.Error(), "snapshot_scan_failed"
|
|
218
|
+
}
|
|
219
|
+
return encodeBinaryResultWithTypes(result.Rows, nil, result.ColumnTypes)
|
|
220
|
+
}
|
|
221
|
+
if frame.TypeID == protocol.BinaryJoinType {
|
|
222
|
+
return handleBinaryJoin(frame.Payload)
|
|
223
|
+
}
|
|
224
|
+
payload, err := decodeBinaryRowsRequest(frame.Payload)
|
|
225
|
+
if err != nil {
|
|
226
|
+
return nil, err.Error(), "invalid_payload"
|
|
227
|
+
}
|
|
228
|
+
result, err := execution.ExecuteRowsPipelineContext(ctx, payload)
|
|
229
|
+
if err != nil {
|
|
230
|
+
return nil, err.Error(), "execution_error"
|
|
231
|
+
}
|
|
232
|
+
return encodeBinaryResult(result.Rows, result.Aggregates)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
func encodeBinaryResult(rows, aggregates []map[string]interface{}) ([]byte, string, string) {
|
|
236
|
+
return encodeBinaryResultWithTypes(rows, aggregates, nil)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
func encodeBinaryResultWithTypes(rows, aggregates []map[string]interface{}, columnTypes map[string]string) ([]byte, string, string) {
|
|
240
|
+
rowBatch, err := protocol.EncodeRows(rows)
|
|
241
|
+
if err != nil {
|
|
242
|
+
return nil, err.Error(), "encoding_error"
|
|
243
|
+
}
|
|
244
|
+
aggregateBatch, err := protocol.EncodeRows(aggregates)
|
|
245
|
+
if err != nil {
|
|
246
|
+
return nil, err.Error(), "encoding_error"
|
|
247
|
+
}
|
|
248
|
+
metadata, _ := json.Marshal(map[string]interface{}{"row_count": len(rows), "has_aggregates": len(aggregates) > 0, "row_batch_bytes": len(rowBatch), "aggregate_batch_bytes": len(aggregateBatch), "column_types": columnTypes})
|
|
249
|
+
var payload bytes.Buffer
|
|
250
|
+
writeUint32(&payload, uint32(len(metadata)))
|
|
251
|
+
payload.Write(metadata)
|
|
252
|
+
writeUint32(&payload, uint32(len(rowBatch)))
|
|
253
|
+
payload.Write(rowBatch)
|
|
254
|
+
writeUint32(&payload, uint32(len(aggregateBatch)))
|
|
255
|
+
payload.Write(aggregateBatch)
|
|
256
|
+
return payload.Bytes(), "", ""
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
func decodeBinaryRowsRequest(payload []byte) (execution.RowsRequest, error) {
|
|
260
|
+
reader := protocol.NewReader(payload)
|
|
261
|
+
metadataLength, err := reader.Uint32()
|
|
262
|
+
if err != nil {
|
|
263
|
+
return execution.RowsRequest{}, errors.New("binary rows request is truncated")
|
|
264
|
+
}
|
|
265
|
+
metadata, err := reader.Take(int(metadataLength))
|
|
266
|
+
if err != nil {
|
|
267
|
+
return execution.RowsRequest{}, errors.New("binary rows request metadata is invalid")
|
|
268
|
+
}
|
|
269
|
+
var request execution.RowsRequest
|
|
270
|
+
decoder := json.NewDecoder(bytes.NewReader(metadata))
|
|
271
|
+
decoder.UseNumber()
|
|
272
|
+
if err := decoder.Decode(&request); err != nil {
|
|
273
|
+
return execution.RowsRequest{}, errors.New("binary rows request metadata is invalid")
|
|
274
|
+
}
|
|
275
|
+
rows, err := protocol.DecodeRows(reader.Bytes())
|
|
276
|
+
if err != nil {
|
|
277
|
+
return execution.RowsRequest{}, err
|
|
278
|
+
}
|
|
279
|
+
request.Rows = rows
|
|
280
|
+
return request, nil
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
func handleBinaryJoin(payload []byte) ([]byte, string, string) {
|
|
284
|
+
reader := protocol.NewReader(payload)
|
|
285
|
+
metadataLength, err := reader.Uint32()
|
|
286
|
+
if err != nil {
|
|
287
|
+
return nil, "binary join request is truncated", "invalid_payload"
|
|
288
|
+
}
|
|
289
|
+
metadata, err := reader.Take(int(metadataLength))
|
|
290
|
+
if err != nil {
|
|
291
|
+
return nil, "binary join metadata is invalid", "invalid_payload"
|
|
292
|
+
}
|
|
293
|
+
var specification struct {
|
|
294
|
+
LeftKey string `json:"left_key"`
|
|
295
|
+
RightKey string `json:"right_key"`
|
|
296
|
+
JoinType string `json:"join_type"`
|
|
297
|
+
Algorithm string `json:"algorithm"`
|
|
298
|
+
}
|
|
299
|
+
if err := json.Unmarshal(metadata, &specification); err != nil || specification.LeftKey == "" || specification.RightKey == "" {
|
|
300
|
+
return nil, "binary join metadata is invalid", "invalid_payload"
|
|
301
|
+
}
|
|
302
|
+
leftLength, err := reader.Uint32()
|
|
303
|
+
if err != nil {
|
|
304
|
+
return nil, "binary join request is truncated", "invalid_payload"
|
|
305
|
+
}
|
|
306
|
+
left, err := reader.Take(int(leftLength))
|
|
307
|
+
if err != nil {
|
|
308
|
+
return nil, "binary join left batch is invalid", "invalid_payload"
|
|
309
|
+
}
|
|
310
|
+
rightLength, err := reader.Uint32()
|
|
311
|
+
if err != nil {
|
|
312
|
+
return nil, "binary join request is truncated", "invalid_payload"
|
|
313
|
+
}
|
|
314
|
+
right, err := reader.Take(int(rightLength))
|
|
315
|
+
if err != nil || reader.Remaining() != 0 {
|
|
316
|
+
return nil, "binary join right batch is invalid", "invalid_payload"
|
|
317
|
+
}
|
|
318
|
+
if specification.JoinType != "" && !equalFold(specification.JoinType, "inner") {
|
|
319
|
+
return nil, "only inner joins are supported by the accelerator", "unsupported_operation"
|
|
320
|
+
}
|
|
321
|
+
leftRows, err := protocol.DecodeRows(left)
|
|
322
|
+
if err != nil {
|
|
323
|
+
return nil, err.Error(), "invalid_payload"
|
|
324
|
+
}
|
|
325
|
+
rightRows, err := protocol.DecodeRows(right)
|
|
326
|
+
if err != nil {
|
|
327
|
+
return nil, err.Error(), "invalid_payload"
|
|
328
|
+
}
|
|
329
|
+
joined := execution.HashJoinRows(leftRows, rightRows, specification.LeftKey, specification.RightKey)
|
|
330
|
+
if equalFold(specification.Algorithm, "merge") {
|
|
331
|
+
joined = execution.MergeJoinRows(leftRows, rightRows, specification.LeftKey, specification.RightKey)
|
|
332
|
+
}
|
|
333
|
+
return encodeBinaryResult(joined, nil)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
func handleSHA256(raw json.RawMessage) response {
|
|
337
|
+
var payload struct {
|
|
338
|
+
DataBase64 string `json:"data_base64"`
|
|
339
|
+
}
|
|
340
|
+
if err := json.Unmarshal(raw, &payload); err != nil {
|
|
341
|
+
return failure("invalid sha256 payload", "invalid_payload")
|
|
342
|
+
}
|
|
343
|
+
data, err := base64.StdEncoding.DecodeString(payload.DataBase64)
|
|
344
|
+
if err != nil {
|
|
345
|
+
return failure("data_base64 is invalid", "invalid_payload")
|
|
346
|
+
}
|
|
347
|
+
return response{Success: true, Type: "sha256_response", Payload: map[string]string{"digest": wal.SHA256Hex(data)}}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
func handleGzip(raw json.RawMessage, compress bool) response {
|
|
351
|
+
var payload struct {
|
|
352
|
+
DataBase64 string `json:"data_base64"`
|
|
353
|
+
Level int `json:"level,omitempty"`
|
|
354
|
+
}
|
|
355
|
+
if err := json.Unmarshal(raw, &payload); err != nil {
|
|
356
|
+
return failure("invalid compression payload", "invalid_payload")
|
|
357
|
+
}
|
|
358
|
+
data, err := base64.StdEncoding.DecodeString(payload.DataBase64)
|
|
359
|
+
if err != nil {
|
|
360
|
+
return failure("data_base64 is invalid", "invalid_payload")
|
|
361
|
+
}
|
|
362
|
+
if compress {
|
|
363
|
+
level := payload.Level
|
|
364
|
+
if level == 0 {
|
|
365
|
+
level = gzip.DefaultCompression
|
|
366
|
+
}
|
|
367
|
+
compressed, err := wal.Gzip(data, level)
|
|
368
|
+
if err != nil {
|
|
369
|
+
return failure("gzip operation failed", "compression_error")
|
|
370
|
+
}
|
|
371
|
+
return response{Success: true, Type: "gzip_response", Payload: map[string]interface{}{"data_base64": base64.StdEncoding.EncodeToString(compressed), "input_bytes": len(data), "output_bytes": len(compressed)}}
|
|
372
|
+
}
|
|
373
|
+
output, err := wal.Gunzip(data, protocol.MaxFrameSize)
|
|
374
|
+
if err != nil {
|
|
375
|
+
return failure("gunzip operation failed", "compression_error")
|
|
376
|
+
}
|
|
377
|
+
return response{Success: true, Type: "gunzip_response", Payload: map[string]interface{}{"data_base64": base64.StdEncoding.EncodeToString(output), "output_bytes": len(output)}}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
func handleRowsPipeline(raw json.RawMessage) response {
|
|
381
|
+
return handleRowsPipelineContext(context.Background(), raw)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
func handleRowsPipelineContext(ctx context.Context, raw json.RawMessage) response {
|
|
385
|
+
var payload execution.RowsRequest
|
|
386
|
+
decoder := json.NewDecoder(bytes.NewReader(raw))
|
|
387
|
+
decoder.UseNumber()
|
|
388
|
+
if err := decoder.Decode(&payload); err != nil {
|
|
389
|
+
return failure("invalid rows pipeline payload", "invalid_payload")
|
|
390
|
+
}
|
|
391
|
+
result, err := execution.ExecuteRowsPipelineContext(ctx, payload)
|
|
392
|
+
if err != nil {
|
|
393
|
+
code := "execution_error"
|
|
394
|
+
if errors.Is(err, context.Canceled) {
|
|
395
|
+
code = "cancelled"
|
|
396
|
+
}
|
|
397
|
+
return failure(err.Error(), code)
|
|
398
|
+
}
|
|
399
|
+
return response{Success: true, Type: "rows_pipeline_response", Payload: result.AsMap()}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
func streamRowsPipeline(ctx context.Context, req request, writer *bufio.Writer, writeMu *sync.Mutex) {
|
|
403
|
+
var payload execution.RowsRequest
|
|
404
|
+
decoder := json.NewDecoder(bytes.NewReader(req.Payload))
|
|
405
|
+
decoder.UseNumber()
|
|
406
|
+
if err := decoder.Decode(&payload); err != nil {
|
|
407
|
+
writeStreamResponse(writer, writeMu, response{ID: req.ID, Type: "rows_pipeline_batch", Success: false, Error: "invalid rows pipeline payload", Code: "invalid_payload"})
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
result, err := execution.ExecuteRowsPipelineContext(ctx, payload)
|
|
411
|
+
if err != nil {
|
|
412
|
+
code := "execution_error"
|
|
413
|
+
if errors.Is(err, context.Canceled) {
|
|
414
|
+
code = "cancelled"
|
|
415
|
+
}
|
|
416
|
+
writeStreamResponse(writer, writeMu, response{ID: req.ID, Type: "rows_pipeline_batch", Success: false, Error: err.Error(), Code: code})
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
batchSize := payload.BatchSize
|
|
420
|
+
if batchSize < 1 {
|
|
421
|
+
batchSize = 1024
|
|
422
|
+
}
|
|
423
|
+
if len(result.Rows) == 0 {
|
|
424
|
+
writeStreamResponse(writer, writeMu, response{ID: req.ID, Type: "rows_pipeline_batch", Success: true, Payload: map[string]interface{}{"rows": []map[string]interface{}{}, "row_count": 0}, More: false})
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
for start := 0; start < len(result.Rows); start += batchSize {
|
|
428
|
+
select {
|
|
429
|
+
case <-ctx.Done():
|
|
430
|
+
writeStreamResponse(writer, writeMu, response{ID: req.ID, Type: "rows_pipeline_batch", Success: false, Error: ctx.Err().Error(), Code: "cancelled"})
|
|
431
|
+
return
|
|
432
|
+
default:
|
|
433
|
+
}
|
|
434
|
+
end := start + batchSize
|
|
435
|
+
if end > len(result.Rows) {
|
|
436
|
+
end = len(result.Rows)
|
|
437
|
+
}
|
|
438
|
+
payload := map[string]interface{}{"rows": result.Rows[start:end], "row_count": end - start}
|
|
439
|
+
if end == len(result.Rows) && len(result.Aggregates) > 0 {
|
|
440
|
+
payload["aggregates"] = result.Aggregates
|
|
441
|
+
}
|
|
442
|
+
writeStreamResponse(writer, writeMu, response{ID: req.ID, Type: "rows_pipeline_batch", Success: true, Payload: payload, More: end < len(result.Rows)})
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
func writeStreamResponse(writer *bufio.Writer, writeMu *sync.Mutex, result response) {
|
|
447
|
+
writeMu.Lock()
|
|
448
|
+
defer writeMu.Unlock()
|
|
449
|
+
_ = protocol.WriteJSONResponse(writer, result)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
func handleWALBatch(raw json.RawMessage) response {
|
|
453
|
+
var payload struct {
|
|
454
|
+
Records []struct {
|
|
455
|
+
Kind uint8 `json:"kind"`
|
|
456
|
+
LSN uint64 `json:"lsn"`
|
|
457
|
+
Transaction uint64 `json:"transaction"`
|
|
458
|
+
PayloadBase64 string `json:"payload_base64"`
|
|
459
|
+
} `json:"records"`
|
|
460
|
+
Compress bool `json:"compress,omitempty"`
|
|
461
|
+
}
|
|
462
|
+
if err := json.Unmarshal(raw, &payload); err != nil || len(payload.Records) == 0 {
|
|
463
|
+
return failure("invalid WAL batch payload", "invalid_payload")
|
|
464
|
+
}
|
|
465
|
+
encoded := make([][]byte, 0, len(payload.Records))
|
|
466
|
+
for _, record := range payload.Records {
|
|
467
|
+
data, err := base64.StdEncoding.DecodeString(record.PayloadBase64)
|
|
468
|
+
if err != nil {
|
|
469
|
+
return failure("WAL payload is not valid base64", "invalid_payload")
|
|
470
|
+
}
|
|
471
|
+
encoded = append(encoded, wal.EncodeRecord(record.Kind, record.LSN, record.Transaction, data))
|
|
472
|
+
}
|
|
473
|
+
joined := bytes.Join(encoded, nil)
|
|
474
|
+
result := map[string]interface{}{
|
|
475
|
+
"record_count": len(encoded),
|
|
476
|
+
"bytes": len(joined),
|
|
477
|
+
"checksum": wal.SHA256Hex(joined),
|
|
478
|
+
"records_base64": base64.StdEncoding.EncodeToString(joined),
|
|
479
|
+
}
|
|
480
|
+
if payload.Compress {
|
|
481
|
+
compressed, err := wal.Gzip(joined, gzip.DefaultCompression)
|
|
482
|
+
if err != nil {
|
|
483
|
+
return failure("WAL compression failed", "compression_error")
|
|
484
|
+
}
|
|
485
|
+
result["compressed_base64"] = base64.StdEncoding.EncodeToString(compressed)
|
|
486
|
+
result["compressed_bytes"] = len(compressed)
|
|
487
|
+
}
|
|
488
|
+
return response{Success: true, Type: "wal_batch_response", Payload: result}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
func writeUint32(output *bytes.Buffer, value uint32) {
|
|
492
|
+
output.WriteByte(byte(value))
|
|
493
|
+
output.WriteByte(byte(value >> 8))
|
|
494
|
+
output.WriteByte(byte(value >> 16))
|
|
495
|
+
output.WriteByte(byte(value >> 24))
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
func equalFold(left, right string) bool {
|
|
499
|
+
if len(left) != len(right) {
|
|
500
|
+
return false
|
|
501
|
+
}
|
|
502
|
+
for index := range left {
|
|
503
|
+
l, r := left[index], right[index]
|
|
504
|
+
if l >= 'A' && l <= 'Z' {
|
|
505
|
+
l += 'a' - 'A'
|
|
506
|
+
}
|
|
507
|
+
if r >= 'A' && r <= 'Z' {
|
|
508
|
+
r += 'a' - 'A'
|
|
509
|
+
}
|
|
510
|
+
if l != r {
|
|
511
|
+
return false
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return true
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
func failure(message, code string) response {
|
|
518
|
+
return response{Success: false, Type: "error", Error: message, Code: code}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const protocolVersion = protocol.Version
|