Skip to content

Commit 32a27d5

Browse files
authored
Merge pull request #264 from carlosms/fix-241
Use mysql KILL to cancel queries
2 parents af56ee8 + 2450581 commit 32a27d5

File tree

5 files changed

+92
-6
lines changed

5 files changed

+92
-6
lines changed

Diff for: server/handler/common_unit_test.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,9 @@ func (suite *HandlerUnitSuite) SetupTest() {
3939
}
4040

4141
func (suite *HandlerUnitSuite) TearDownTest() {
42-
suite.db.Close()
42+
defer suite.db.Close()
43+
44+
if err := suite.mock.ExpectationsWereMet(); err != nil {
45+
suite.FailNowf("there were unfulfilled expectations:", err.Error())
46+
}
4347
}

Diff for: server/handler/query.go

+66-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,31 @@ func Query(db service.SQLDB) RequestProcessFunc {
6666
}
6767

6868
query, limitSet := addLimit(queryRequest.Query, queryRequest.Limit)
69-
rows, err := db.QueryContext(r.Context(), query)
69+
70+
// go-sql-driver/mysql QueryContext stops waiting for the query results on
71+
// context cancel, but it does not actually cancel the query on the server
72+
73+
c := make(chan error, 1)
74+
75+
var rows *sql.Rows
76+
go func() {
77+
rows, err = db.QueryContext(r.Context(), query)
78+
c <- err
79+
}()
80+
81+
// It may happen that the QueryContext returns with an error because of
82+
// context cancellation. In this case, the select may enter on the second
83+
// case. We check if the context was cancelled with Err() instead of Done()
84+
select {
85+
case <-r.Context().Done():
86+
case err = <-c:
87+
}
88+
89+
if r.Context().Err() != nil {
90+
killQuery(db, query)
91+
return nil, dbError(r.Context().Err())
92+
}
93+
7094
if err != nil {
7195
return nil, dbError(err)
7296
}
@@ -103,6 +127,47 @@ func Query(db service.SQLDB) RequestProcessFunc {
103127
}
104128
}
105129

130+
func killQuery(db service.SQLDB, query string) {
131+
pRows, pErr := db.Query("SHOW FULL PROCESSLIST")
132+
if pErr != nil {
133+
// TODO (carlosms) log error when we migrate to go-log
134+
return
135+
}
136+
defer pRows.Close()
137+
138+
found := false
139+
var foundID int
140+
141+
for pRows.Next() {
142+
var id int
143+
var info sql.NullString
144+
var rb sql.RawBytes
145+
// The columns are:
146+
// Id, User, Host, db, Command, Time, State, Info
147+
// gitbase returns the query on "Info".
148+
if err := pRows.Scan(&id, &rb, &rb, &rb, &rb, &rb, &rb, &info); err != nil {
149+
// TODO (carlosms) log error when we migrate to go-log
150+
return
151+
}
152+
153+
if info.Valid && info.String == query {
154+
if found {
155+
// Found more than one match for current query, we cannot know which
156+
// one is ours. Skip the cancellation
157+
// TODO (carlosms) log error when we migrate to go-log
158+
return
159+
}
160+
161+
found = true
162+
foundID = id
163+
}
164+
}
165+
166+
if found {
167+
db.Exec(fmt.Sprintf("KILL %d", foundID))
168+
}
169+
}
170+
106171
// columnsInfo returns the column names and column types, or error
107172
func columnsInfo(rows *sql.Rows) ([]string, []string, error) {
108173
names, err := rows.Columns()

Diff for: server/handler/query_test.go

+15-4
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,6 @@ func (suite *QuerySuite) TestBadRequest() {
8383
`{"query": "select * from repositories", "limit": "string"}`,
8484
}
8585

86-
suite.mock.ExpectQuery(".*").WillReturnError(fmt.Errorf("forced err"))
87-
8886
for _, tc := range testCases {
8987
suite.T().Run(tc, func(t *testing.T) {
9088
a := assert.New(t)
@@ -201,8 +199,16 @@ func (suite *QuerySuite) TestQueryAbort() {
201199
// Ideally we would test that the sql query context is canceled, but
202200
// go-sqlmock does not have something like ExpectContextCancellation
203201

204-
mockRows := sqlmock.NewRows([]string{"a", "b", "c", "d"})
205-
suite.mock.ExpectQuery(".*").WillDelayFor(2 * time.Second).WillReturnRows(mockRows)
202+
mockRows := sqlmock.NewRows([]string{"a", "b", "c", "d"}).AddRow(1, "one", 1.5, 100)
203+
suite.mock.ExpectQuery(`select \* from repositories`).WillDelayFor(2 * time.Second).WillReturnRows(mockRows)
204+
205+
mockProcessRows := sqlmock.NewRows(
206+
[]string{"Id", "User", "Host", "db", "Command", "Time", "State", "Info"}).
207+
AddRow(1234, nil, "localhost:3306", nil, "query", 2, "SquashedTable(refs, commit_files, files)(1/5)", "select * from files").
208+
AddRow(1288, nil, "localhost:3306", nil, "query", 2, "SquashedTable(refs, commit_files, files)(1/5)", "select * from repositories")
209+
suite.mock.ExpectQuery("SHOW FULL PROCESSLIST").WillReturnRows(mockProcessRows)
210+
211+
suite.mock.ExpectExec("KILL 1288")
206212

207213
json := `{"query": "select * from repositories"}`
208214
req, _ := http.NewRequest("POST", "/query", strings.NewReader(json))
@@ -227,6 +233,11 @@ func (suite *QuerySuite) TestQueryAbort() {
227233
handler.ServeHTTP(res, req)
228234
}()
229235

236+
// Without this wait the Request is cancelled before the handler has time to
237+
// start the query. Which also works fine, but we want to test a cancellation
238+
// for a query that is in progress
239+
time.Sleep(200 * time.Millisecond)
240+
230241
cancel()
231242

232243
wg.Wait()

Diff for: server/service/common.go

+1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ type SQLDB interface {
1212
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
1313
QueryRow(query string, args ...interface{}) *sql.Row
1414
Ping() error
15+
Exec(query string, args ...interface{}) (sql.Result, error)
1516
}

Diff for: server/testing/common.go

+5
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ func (db *MockDB) QueryRow(query string, args ...interface{}) *sql.Row {
3333
return nil
3434
}
3535

36+
// Exec executes a query without returning any rows
37+
func (db *MockDB) Exec(query string, args ...interface{}) (sql.Result, error) {
38+
return nil, nil
39+
}
40+
3641
// As returned by gitbase v0.17.0-rc.4, SELECT UAST('console.log("test")', 'JavaScript') AS uast
3742
const (
3843
UASTMarshaled = "\x00\x00\x02\x16\n\x04File\x1a\xfc\x03\n\aProgram\x12\x17\n\finternalRole\x12\aprogram\x12\x14\n\nsourceType\x12\x06module\x1a\xb0\x03\n\x13ExpressionStatement\x12\x14\n\finternalRole\x12\x04body\x1a\xf1\x02\n\x0eCallExpression\x12\x1a\n\finternalRole\x12\nexpression\x1a\xdc\x01\n\x10MemberExpression\x12\x16\n\finternalRole\x12\x06callee\x12\x11\n\bcomputed\x12\x05false\x1aC\n\nIdentifier\x12\x16\n\finternalRole\x12\x06object\x12\x0f\n\x04Name\x12\aconsole*\x04\x10\x01\x18\x012\x06\b\a\x10\x01\x18\b\x1aC\n\nIdentifier\x12\v\n\x04Name\x12\x03log\x12\x18\n\finternalRole\x12\bproperty*\x06\b\b\x10\x01\x18\t2\x06\b\v\x10\x01\x18\f*\x04\x10\x01\x18\x012\x06\b\v\x10\x01\x18\f:\x05\x02\x12\x01TU\x1aR\n\x06String\x12\r\n\x05Value\x12\x04test\x12\n\n\x06Format\x12\x00\x12\x19\n\finternalRole\x12\targuments*\x06\b\f\x10\x01\x18\r2\x06\b\x12\x10\x01\x18\x13:\x02T1*\x04\x10\x01\x18\x012\x06\b\x13\x10\x01\x18\x14:\x02\x12T*\x04\x10\x01\x18\x012\x06\b\x13\x10\x01\x18\x14:\x01\x13*\x04\x10\x01\x18\x012\x06\b\x13\x10\x01\x18\x14:\x019*\x04\x10\x01\x18\x012\x06\b\x13\x10\x01\x18\x14:\x01\""

0 commit comments

Comments
 (0)