forked from apache/cassandra-gocql-driver
-
Notifications
You must be signed in to change notification settings - Fork 59
/
Copy pathquery_executor.go
238 lines (206 loc) · 6.16 KB
/
query_executor.go
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
package gocql
import (
"context"
"errors"
"sync"
"time"
)
type ExecutableQuery interface {
borrowForExecution() // Used to ensure that the query stays alive for lifetime of a particular execution goroutine.
releaseAfterExecution() // Used when a goroutine finishes its execution attempts, either with ok result or an error.
execute(ctx context.Context, conn *Conn) *Iter
attempt(keyspace string, end, start time.Time, iter *Iter, host *HostInfo)
retryPolicy() RetryPolicy
speculativeExecutionPolicy() SpeculativeExecutionPolicy
GetRoutingKey() ([]byte, error)
Keyspace() string
Table() string
IsIdempotent() bool
IsLWT() bool
GetCustomPartitioner() Partitioner
withContext(context.Context) ExecutableQuery
RetryableQuery
GetSession() *Session
}
type queryExecutor struct {
pool *policyConnPool
policy HostSelectionPolicy
}
func (q *queryExecutor) attemptQuery(ctx context.Context, qry ExecutableQuery, conn *Conn) *Iter {
start := time.Now()
iter := qry.execute(ctx, conn)
end := time.Now()
qry.attempt(q.pool.keyspace, end, start, iter, conn.host)
return iter
}
func (q *queryExecutor) speculate(ctx context.Context, qry ExecutableQuery, sp SpeculativeExecutionPolicy,
hostIter NextHost, results chan *Iter) *Iter {
ticker := time.NewTicker(sp.Delay())
defer ticker.Stop()
for i := 0; i < sp.Attempts(); i++ {
select {
case <-ticker.C:
qry.borrowForExecution() // ensure liveness in case of executing Query to prevent races with Query.Release().
go q.run(ctx, qry, hostIter, results)
case <-ctx.Done():
return &Iter{err: ctx.Err()}
case iter := <-results:
return iter
}
}
return nil
}
func (q *queryExecutor) executeQuery(qry ExecutableQuery) (*Iter, error) {
hostIter := q.policy.Pick(qry)
// check if the query is not marked as idempotent, if
// it is, we force the policy to NonSpeculative
sp := qry.speculativeExecutionPolicy()
if !qry.IsIdempotent() || sp.Attempts() == 0 {
return q.do(qry.Context(), qry, hostIter), nil
}
// When speculative execution is enabled, we could be accessing the host iterator from multiple goroutines below.
// To ensure we don't call it concurrently, we wrap the returned NextHost function here to synchronize access to it.
var mu sync.Mutex
origHostIter := hostIter
hostIter = func() SelectedHost {
mu.Lock()
defer mu.Unlock()
return origHostIter()
}
ctx, cancel := context.WithCancel(qry.Context())
defer cancel()
results := make(chan *Iter, 1)
// Launch the main execution
qry.borrowForExecution() // ensure liveness in case of executing Query to prevent races with Query.Release().
go q.run(ctx, qry, hostIter, results)
// The speculative executions are launched _in addition_ to the main
// execution, on a timer. So Speculation{2} would make 3 executions running
// in total.
if iter := q.speculate(ctx, qry, sp, hostIter, results); iter != nil {
return iter, nil
}
select {
case iter := <-results:
return iter, nil
case <-ctx.Done():
return &Iter{err: ctx.Err()}, nil
}
}
func (q *queryExecutor) do(ctx context.Context, qry ExecutableQuery, hostIter NextHost) *Iter {
rt := qry.retryPolicy()
if rt == nil {
rt = &SimpleRetryPolicy{3}
}
lwtRT, isRTSupportsLWT := rt.(LWTRetryPolicy)
var getShouldRetry func(qry RetryableQuery) bool
var getRetryType func(error) RetryType
if isRTSupportsLWT && qry.IsLWT() {
getShouldRetry = lwtRT.AttemptLWT
getRetryType = lwtRT.GetRetryTypeLWT
} else {
getShouldRetry = rt.Attempt
getRetryType = rt.GetRetryType
}
var potentiallyExecuted bool
execute := func(qry ExecutableQuery, selectedHost SelectedHost) (iter *Iter, retry RetryType) {
host := selectedHost.Info()
if host == nil || !host.IsUp() {
return &Iter{
err: &QueryError{
err: ErrHostDown,
potentiallyExecuted: potentiallyExecuted,
},
}, RetryNextHost
}
pool, ok := q.pool.getPool(host)
if !ok {
return &Iter{
err: &QueryError{
err: ErrNoPool,
potentiallyExecuted: potentiallyExecuted,
},
}, RetryNextHost
}
conn := pool.Pick(selectedHost.Token(), qry)
if conn == nil {
return &Iter{
err: &QueryError{
err: ErrNoConnectionsInPool,
potentiallyExecuted: potentiallyExecuted,
},
}, RetryNextHost
}
iter = q.attemptQuery(ctx, qry, conn)
iter.host = selectedHost.Info()
// Update host
if iter.err == nil {
return iter, RetryType(255)
}
switch {
case errors.Is(iter.err, context.Canceled),
errors.Is(iter.err, context.DeadlineExceeded):
selectedHost.Mark(nil)
potentiallyExecuted = true
retry = Rethrow
default:
selectedHost.Mark(iter.err)
retry = RetryType(255) // Don't enforce retry and get it from retry policy
}
var qErr *QueryError
if errors.As(iter.err, &qErr) {
potentiallyExecuted = potentiallyExecuted && qErr.PotentiallyExecuted()
qErr.potentiallyExecuted = potentiallyExecuted
qErr.isIdempotent = qry.IsIdempotent()
iter.err = qErr
} else {
iter.err = &QueryError{
err: iter.err,
potentiallyExecuted: potentiallyExecuted,
isIdempotent: qry.IsIdempotent(),
}
}
return iter, retry
}
var lastErr error
selectedHost := hostIter()
for selectedHost != nil {
iter, retryType := execute(qry, selectedHost)
if iter.err == nil {
return iter
}
lastErr = iter.err
// Exit if retry policy decides to not retry anymore
if retryType == RetryType(255) {
if !getShouldRetry(qry) {
return iter
}
retryType = getRetryType(iter.err)
}
// If query is unsuccessful, check the error with RetryPolicy to retry
switch retryType {
case Retry:
// retry on the same host
continue
case Rethrow, Ignore:
return iter
case RetryNextHost:
// retry on the next host
selectedHost = hostIter()
continue
default:
// Undefined? Return nil and error, this will panic in the requester
return &Iter{err: ErrUnknownRetryType}
}
}
if lastErr != nil {
return &Iter{err: lastErr}
}
return &Iter{err: ErrNoConnections}
}
func (q *queryExecutor) run(ctx context.Context, qry ExecutableQuery, hostIter NextHost, results chan<- *Iter) {
select {
case results <- q.do(ctx, qry, hostIter):
case <-ctx.Done():
}
qry.releaseAfterExecution()
}