@@ -5,6 +5,7 @@ package integration
55
66import (
77 "net/url"
8+ "sync"
89 "testing"
910 "testing/fstest"
1011
@@ -13,10 +14,12 @@ import (
1314 unit_model "forgejo.org/models/unit"
1415 "forgejo.org/models/unittest"
1516 user_model "forgejo.org/models/user"
17+ "forgejo.org/modules/container"
1618 "forgejo.org/modules/setting"
1719 "forgejo.org/modules/util"
1820 "forgejo.org/tests/forgery"
1921
22+ runnerv1 "code.forgejo.org/forgejo/actions-proto/runner/v1"
2023 "code.forgejo.org/xorm/xorm/convert"
2124 "github.com/stretchr/testify/assert"
2225 "github.com/stretchr/testify/require"
@@ -201,6 +204,91 @@ jobs:
201204 })
202205}
203206
207+ func TestActionFetchTask_IdempotentConcurrent (t * testing.T ) {
208+ if ! setting .Database .Type .IsSQLite3 () {
209+ // mock repo runner only supported on SQLite testing
210+ t .Skip ()
211+ }
212+
213+ onApplicationRun (t , func (t * testing.T , u * url.URL ) {
214+ user2 := unittest .AssertExistsAndLoadBean (t , & user_model.User {ID : 2 })
215+
216+ // create the repo
217+ repo := createFetchTaskTestRepository (t , user2 , "matrix.yml" , `
218+ on:
219+ push:
220+ jobs:
221+ job1:
222+ strategy:
223+ matrix:
224+ d1: [a, b, c, d, e]
225+ d2: [a, b, c, d, e]
226+ runs-on: ubuntu-latest
227+ steps:
228+ - run: sleep 2
229+ ` )
230+
231+ runner := newMockRunner ()
232+ runner .registerAsRepoRunner (t , user2 .Name , repo .Name , "mock-runner" , []string {"ubuntu-latest" })
233+
234+ runner .setRequestKey ("c6dacc80-dace-4cea-9aad-f0e266355d8e" )
235+
236+ // If we make two simultaneous requests with the same runner request key, we should get either the error
237+ // "request key is currently locked; retry soon", or, the same tasks from both requests.
238+ concurrentCount := 15
239+ type fetchResult struct {
240+ index int
241+ task * runnerv1.Task
242+ addtTasks []* runnerv1.Task
243+ err error
244+ }
245+ fetchResults := make (chan fetchResult , concurrentCount )
246+
247+ var wg sync.WaitGroup
248+ for i := range concurrentCount {
249+ wg .Go (func () {
250+ // Larger task capacity is used to make the successful call take longer, cause higher chance of problems if
251+ // concurrency isn't handled correctly
252+ task , addtTasks , err := runner .fetchTaskOrError (t , 10 )
253+ fetchResults <- fetchResult {index : i , task : task , addtTasks : addtTasks , err : err }
254+ })
255+ }
256+
257+ wg .Wait ()
258+ close (fetchResults )
259+
260+ var firstResponseTaskIDs container.Set [int64 ]
261+ for res := range fetchResults {
262+ t .Logf ("res = %#v" , res )
263+ if res .task != nil {
264+ // This response had tasks, so let's ensure they're always the same for every response.
265+ taskIDs := container.Set [int64 ]{}
266+ taskIDs .Add (res .task .GetId ())
267+ for _ , extraTask := range res .addtTasks {
268+ assert .True (t , taskIDs .Add (extraTask .GetId ()))
269+ }
270+ if firstResponseTaskIDs == nil {
271+ // first response with tasks -- record the IDs
272+ firstResponseTaskIDs = taskIDs
273+ assert .Len (t , taskIDs , 10 )
274+ } else {
275+ // we've already found one response with tasks, so assert that they're all the same
276+ d1 := firstResponseTaskIDs .Difference (taskIDs )
277+ assert .Empty (t , d1 , "first response taskIDs minus current response taskIDs should be empty" )
278+ d2 := taskIDs .Difference (firstResponseTaskIDs )
279+ assert .Empty (t , d2 , "current response taskIDs minus first response taskIDs should be empty" )
280+ }
281+ } else if res .err != nil {
282+ require .ErrorContains (t , res .err , "request key is currently locked" )
283+ } else {
284+ assert .Fail (t , "unexpected condition - res.task = nil, res.err = nil" )
285+ }
286+ }
287+
288+ assert .NotNil (t , firstResponseTaskIDs , "at least one response should return tasks" )
289+ })
290+ }
291+
204292func TestActionFetchTask_RequestedJob (t * testing.T ) {
205293 if ! setting .Database .Type .IsSQLite3 () {
206294 // mock repo runner only supported on SQLite testing
0 commit comments