@@ -20,6 +20,7 @@ import (
20
20
"context"
21
21
"fmt"
22
22
"strings"
23
+ "sync"
23
24
"sync/atomic"
24
25
25
26
"vitess.io/vitess/go/sqltypes"
@@ -115,22 +116,31 @@ func bindvarForType(t querypb.Type) *querypb.BindVariable {
115
116
116
117
// TryStreamExecute performs a streaming exec.
117
118
func (jn * Join ) TryStreamExecute (ctx context.Context , vcursor VCursor , bindVars map [string ]* querypb.BindVariable , wantfields bool , callback func (* sqltypes.Result ) error ) error {
118
- var fieldNeeded atomic.Bool
119
- fieldNeeded .Store (wantfields )
120
- err := vcursor .StreamExecutePrimitive (ctx , jn .Left , bindVars , fieldNeeded .Load (), func (lresult * sqltypes.Result ) error {
119
+ var mu sync.Mutex
120
+ // We need to use this atomic since we're also reading this
121
+ // value outside of it being locked with the mu lock.
122
+ // This is still racy, but worst case it means that we may
123
+ // retrieve the right hand side fields twice instead of once.
124
+ var fieldsSent atomic.Bool
125
+ fieldsSent .Store (! wantfields )
126
+ err := vcursor .StreamExecutePrimitive (ctx , jn .Left , bindVars , wantfields , func (lresult * sqltypes.Result ) error {
121
127
joinVars := make (map [string ]* querypb.BindVariable )
122
128
for _ , lrow := range lresult .Rows {
123
129
for k , col := range jn .Vars {
124
130
joinVars [k ] = sqltypes .ValueBindVariable (lrow [col ])
125
131
}
126
132
var rowSent atomic.Bool
127
- err := vcursor .StreamExecutePrimitive (ctx , jn .Right , combineVars (bindVars , joinVars ), fieldNeeded .Load (), func (rresult * sqltypes.Result ) error {
133
+ err := vcursor .StreamExecutePrimitive (ctx , jn .Right , combineVars (bindVars , joinVars ), ! fieldsSent .Load (), func (rresult * sqltypes.Result ) error {
134
+ // This needs to be locking since it's not safe to just use
135
+ // fieldsSent. This is because we can't have a race between
136
+ // checking fieldsSent and then actually calling the callback
137
+ // and in parallel another goroutine doing the same. That
138
+ // can lead to out of order execution of the callback. So the callback
139
+ // itself and the check need to be covered by the same lock.
140
+ mu .Lock ()
141
+ defer mu .Unlock ()
128
142
result := & sqltypes.Result {}
129
- if fieldNeeded .Load () {
130
- // This code is currently unreachable because the first result
131
- // will always be just the field info, which will cause the outer
132
- // wantfields code path to be executed. But this may change in the future.
133
- fieldNeeded .Store (false )
143
+ if fieldsSent .CompareAndSwap (false , true ) {
134
144
result .Fields = joinFields (lresult .Fields , rresult .Fields , jn .Cols )
135
145
}
136
146
for _ , rrow := range rresult .Rows {
@@ -154,8 +164,15 @@ func (jn *Join) TryStreamExecute(ctx context.Context, vcursor VCursor, bindVars
154
164
return callback (result )
155
165
}
156
166
}
157
- if fieldNeeded .Load () {
158
- fieldNeeded .Store (false )
167
+ // This needs to be locking since it's not safe to just use
168
+ // fieldsSent. This is because we can't have a race between
169
+ // checking fieldsSent and then actually calling the callback
170
+ // and in parallel another goroutine doing the same. That
171
+ // can lead to out of order execution of the callback. So the callback
172
+ // itself and the check need to be covered by the same lock.
173
+ mu .Lock ()
174
+ defer mu .Unlock ()
175
+ if fieldsSent .CompareAndSwap (false , true ) {
159
176
for k := range jn .Vars {
160
177
joinVars [k ] = sqltypes .NullBindVariable
161
178
}
0 commit comments