Skip to content

Commit 44cd4ee

Browse files
committed
fix(github_graphql): remove stale local issue rows
1 parent 887257b commit 44cd4ee

4 files changed

Lines changed: 236 additions & 6 deletions

File tree

backend/helpers/pluginhelper/api/graphql_collector.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -276,14 +276,18 @@ func (collector *GraphqlCollector) fetchAsync(reqData *GraphqlRequestData, handl
276276
}
277277
if len(dataErrors) > 0 {
278278
if !collector.args.IgnoreQueryErrors {
279+
hasNonIgnorableDataErrors := false
279280
for _, dataError := range dataErrors {
280-
if strings.Contains(dataError.Error(), "Could not resolve to an Issue") {
281-
logger.Warn(nil, "Issue may have been transferred.")
282-
} else {
283-
collector.checkError(errors.Default.Wrap(dataError, `graphql query got error`))
281+
if isIgnorableGraphqlQueryError(dataError) {
282+
logger.Warn(nil, "Issue may have been transferred or deleted.")
283+
continue
284284
}
285+
hasNonIgnorableDataErrors = true
286+
collector.checkError(errors.Default.Wrap(dataError, `graphql query got error`))
287+
}
288+
if hasNonIgnorableDataErrors {
289+
return
285290
}
286-
return
287291
}
288292
// else: error will deal by ResponseParserWithDataErrors
289293
}
@@ -344,4 +348,8 @@ func (collector *GraphqlCollector) HasError() bool {
344348
return len(collector.workerErrors) > 0
345349
}
346350

351+
func isIgnorableGraphqlQueryError(err error) bool {
352+
return err != nil && strings.Contains(err.Error(), "Could not resolve to an Issue")
353+
}
354+
347355
var _ plugin.SubTask = (*GraphqlCollector)(nil)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package api
19+
20+
import (
21+
"errors"
22+
"testing"
23+
24+
"github.com/stretchr/testify/assert"
25+
)
26+
27+
func TestIsIgnorableGraphqlQueryError(t *testing.T) {
28+
assert.True(t, isIgnorableGraphqlQueryError(errors.New("Could not resolve to an Issue with the number of 17.")))
29+
assert.False(t, isIgnorableGraphqlQueryError(errors.New("some other graphql error")))
30+
assert.False(t, isIgnorableGraphqlQueryError(nil))
31+
}

backend/plugins/github_graphql/tasks/issue_collector.go

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@ package tasks
2020
import (
2121
"encoding/json"
2222
"reflect"
23+
"sort"
2324
"strings"
2425
"time"
2526

2627
"github.com/apache/incubator-devlake/core/dal"
2728
"github.com/apache/incubator-devlake/core/errors"
29+
"github.com/apache/incubator-devlake/core/log"
30+
"github.com/apache/incubator-devlake/core/models/common"
2831
"github.com/apache/incubator-devlake/core/plugin"
2932
"github.com/apache/incubator-devlake/core/utils"
3033
"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
@@ -49,7 +52,8 @@ type GraphqlQueryIssueWrapper struct {
4952
}
5053

5154
type GraphqlQueryIssueDetailWrapper struct {
52-
RateLimit struct {
55+
requestedIssues map[int]missingGithubIssueRef
56+
RateLimit struct {
5357
Cost int
5458
}
5559
Repository struct {
@@ -84,6 +88,13 @@ type GraphqlQueryIssue struct {
8488
} `graphql:"labels(first: 100)"`
8589
}
8690

91+
type missingGithubIssueRef struct {
92+
ConnectionId uint64
93+
GithubId int
94+
Number int
95+
RawDataOrigin common.RawDataOrigin
96+
}
97+
8798
var CollectIssuesMeta = plugin.SubTaskMeta{
8899
Name: "Collect Issues",
89100
EntryPoint: CollectIssues,
@@ -175,12 +186,19 @@ func CollectIssues(taskCtx plugin.SubTaskContext) errors.Error {
175186
ownerName := strings.Split(data.Options.Name, "/")
176187
inputIssues := reqData.Input.([]interface{})
177188
outputIssues := []map[string]interface{}{}
189+
query.requestedIssues = make(map[int]missingGithubIssueRef, len(inputIssues))
178190
for _, i := range inputIssues {
179191
inputIssue := i.(*models.GithubIssue)
180192
outputIssues = append(outputIssues, map[string]interface{}{
181193
`number`: graphql.Int(inputIssue.Number),
182194
})
183195
issueUpdatedAt[inputIssue.Number] = inputIssue.GithubUpdatedAt
196+
query.requestedIssues[inputIssue.Number] = missingGithubIssueRef{
197+
ConnectionId: inputIssue.ConnectionId,
198+
GithubId: inputIssue.GithubId,
199+
Number: inputIssue.Number,
200+
RawDataOrigin: inputIssue.RawDataOrigin,
201+
}
184202
}
185203
variables := map[string]interface{}{
186204
"issue": outputIssues,
@@ -193,10 +211,17 @@ func CollectIssues(taskCtx plugin.SubTaskContext) errors.Error {
193211
query := queryWrapper.(*GraphqlQueryIssueDetailWrapper)
194212
issues := query.Repository.Issues
195213
for _, rawL := range issues {
214+
if rawL.DatabaseId == 0 || rawL.Number == 0 {
215+
continue
216+
}
196217
if rawL.UpdatedAt.After(issueUpdatedAt[rawL.Number]) {
197218
messages = append(messages, errors.Must1(json.Marshal(rawL)))
198219
}
199220
}
221+
missingIssues := findMissingGithubIssues(query.requestedIssues, issues)
222+
if len(missingIssues) > 0 {
223+
err = cleanupMissingGithubIssues(db, taskCtx.GetLogger(), missingIssues)
224+
}
200225
return
201226
},
202227
})
@@ -206,3 +231,95 @@ func CollectIssues(taskCtx plugin.SubTaskContext) errors.Error {
206231

207232
return apiCollector.Execute()
208233
}
234+
235+
func findMissingGithubIssues(requestedIssues map[int]missingGithubIssueRef, resolvedIssues []GraphqlQueryIssue) []missingGithubIssueRef {
236+
if len(requestedIssues) == 0 {
237+
return nil
238+
}
239+
240+
resolvedNumbers := make(map[int]struct{}, len(resolvedIssues))
241+
for _, issue := range resolvedIssues {
242+
if issue.DatabaseId == 0 || issue.Number == 0 {
243+
continue
244+
}
245+
resolvedNumbers[issue.Number] = struct{}{}
246+
}
247+
248+
missingIssues := make([]missingGithubIssueRef, 0)
249+
for number, issue := range requestedIssues {
250+
if _, ok := resolvedNumbers[number]; ok {
251+
continue
252+
}
253+
missingIssues = append(missingIssues, issue)
254+
}
255+
sort.Slice(missingIssues, func(i, j int) bool {
256+
return missingIssues[i].Number < missingIssues[j].Number
257+
})
258+
return missingIssues
259+
}
260+
261+
func cleanupMissingGithubIssues(db dal.Dal, logger log.Logger, issues []missingGithubIssueRef) errors.Error {
262+
var allErrors []error
263+
for _, issue := range issues {
264+
logger.Warn(nil, "GitHub issue #%d no longer resolves from the source API, deleting stale local data", issue.Number)
265+
err := cleanupMissingGithubIssue(db, issue)
266+
if err != nil {
267+
allErrors = append(allErrors, err)
268+
}
269+
}
270+
return errors.Default.Combine(allErrors)
271+
}
272+
273+
func cleanupMissingGithubIssue(db dal.Dal, issue missingGithubIssueRef) errors.Error {
274+
deleteByIssueId := func(model any, table string) errors.Error {
275+
err := db.Delete(model, dal.From(table), dal.Where("connection_id = ? AND issue_id = ?", issue.ConnectionId, issue.GithubId))
276+
if err != nil {
277+
return errors.Default.Wrap(err, "failed to delete stale github issue data from "+table)
278+
}
279+
return nil
280+
}
281+
282+
err := deleteByIssueId(&models.GithubIssueComment{}, models.GithubIssueComment{}.TableName())
283+
if err != nil {
284+
return err
285+
}
286+
err = deleteByIssueId(&models.GithubIssueEvent{}, models.GithubIssueEvent{}.TableName())
287+
if err != nil {
288+
return err
289+
}
290+
err = deleteByIssueId(&models.GithubIssueLabel{}, models.GithubIssueLabel{}.TableName())
291+
if err != nil {
292+
return err
293+
}
294+
err = deleteByIssueId(&models.GithubIssueAssignee{}, models.GithubIssueAssignee{}.TableName())
295+
if err != nil {
296+
return err
297+
}
298+
err = db.Delete(
299+
&models.GithubPrIssue{},
300+
dal.From(models.GithubPrIssue{}.TableName()),
301+
dal.Where("connection_id = ? AND issue_id = ?", issue.ConnectionId, issue.GithubId),
302+
)
303+
if err != nil {
304+
return errors.Default.Wrap(err, "failed to delete stale github pull request issue links")
305+
}
306+
err = db.Delete(
307+
&models.GithubIssue{},
308+
dal.From(models.GithubIssue{}.TableName()),
309+
dal.Where("connection_id = ? AND github_id = ?", issue.ConnectionId, issue.GithubId),
310+
)
311+
if err != nil {
312+
return errors.Default.Wrap(err, "failed to delete stale github issue")
313+
}
314+
if issue.RawDataOrigin.RawDataTable != "" && issue.RawDataOrigin.RawDataId != 0 {
315+
err = db.Delete(
316+
&api.RawData{},
317+
dal.From(issue.RawDataOrigin.RawDataTable),
318+
dal.Where("id = ?", issue.RawDataOrigin.RawDataId),
319+
)
320+
if err != nil {
321+
return errors.Default.Wrap(err, "failed to delete stale raw github issue")
322+
}
323+
}
324+
return nil
325+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package tasks
19+
20+
import (
21+
"testing"
22+
23+
"github.com/apache/incubator-devlake/core/models/common"
24+
"github.com/stretchr/testify/assert"
25+
)
26+
27+
func TestFindMissingGithubIssues(t *testing.T) {
28+
requestedIssues := map[int]missingGithubIssueRef{
29+
17: {
30+
ConnectionId: 1,
31+
GithubId: 1700,
32+
Number: 17,
33+
RawDataOrigin: common.RawDataOrigin{
34+
RawDataTable: "_raw_github_graphql_issues",
35+
RawDataId: 10,
36+
},
37+
},
38+
18: {
39+
ConnectionId: 1,
40+
GithubId: 1800,
41+
Number: 18,
42+
},
43+
}
44+
45+
resolvedIssues := []GraphqlQueryIssue{
46+
{DatabaseId: 1800, Number: 18},
47+
}
48+
49+
missingIssues := findMissingGithubIssues(requestedIssues, resolvedIssues)
50+
51+
if assert.Len(t, missingIssues, 1) {
52+
assert.Equal(t, 17, missingIssues[0].Number)
53+
assert.Equal(t, 1700, missingIssues[0].GithubId)
54+
assert.Equal(t, uint64(10), missingIssues[0].RawDataOrigin.RawDataId)
55+
}
56+
}
57+
58+
func TestFindMissingGithubIssuesSkipsZeroValueResponses(t *testing.T) {
59+
requestedIssues := map[int]missingGithubIssueRef{
60+
17: {Number: 17},
61+
18: {Number: 18},
62+
}
63+
64+
resolvedIssues := []GraphqlQueryIssue{
65+
{},
66+
{DatabaseId: 1800, Number: 18},
67+
}
68+
69+
missingIssues := findMissingGithubIssues(requestedIssues, resolvedIssues)
70+
71+
if assert.Len(t, missingIssues, 1) {
72+
assert.Equal(t, 17, missingIssues[0].Number)
73+
}
74+
}

0 commit comments

Comments
 (0)