chore: initialize Gitea v1.27.2 fork
giteabot backport / giteabot (push) Canceled after 0s
giteabot / giteabot (push) Canceled after 0s
release-nightly / nightly-binary (push) Canceled after 0s
release-nightly / nightly-container (push) Canceled after 0s
cache-seeder / gobuild (push) Canceled after 0s
cache-seeder / lint (bindata, lint-backend) (push) Canceled after 0s
release-nightly-snapcraft / build-and-publish (push) Canceled after 0s
giteabot backport / giteabot (push) Canceled after 0s
giteabot / giteabot (push) Canceled after 0s
release-nightly / nightly-binary (push) Canceled after 0s
release-nightly / nightly-container (push) Canceled after 0s
cache-seeder / gobuild (push) Canceled after 0s
cache-seeder / lint (bindata, lint-backend) (push) Canceled after 0s
release-nightly-snapcraft / build-and-publish (push) Canceled after 0s
Includes direct password setup links in registration emails. Assisted-by: Codex:GPT-5
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// minimalWorkflowPayload returns the minimal YAML for a single-job workflow with no steps.
|
||||
func minimalConcurrentWorkflowPayload(jobID string) []byte {
|
||||
return []byte("on: push\njobs:\n " + jobID + ":\n runs-on: ubuntu-latest\n")
|
||||
}
|
||||
|
||||
// TestCreateTaskForRunnerConcurrentClaim verifies that when multiple runners
|
||||
// poll simultaneously and all initially see the same first waiting job,
|
||||
// each runner claims a distinct job rather than all but one being left
|
||||
// empty-handed. This is the regression test for the race condition where
|
||||
// runners losing the optimistic-lock on job #1 would receive latestVersion
|
||||
// and never retry the remaining 49+ jobs.
|
||||
//
|
||||
// It lives in tests/integration rather than a unit test because SQLite
|
||||
// serializes write transactions, so the contended optimistic-lock path this
|
||||
// guards only runs concurrently against MySQL/PostgreSQL in CI.
|
||||
func TestCreateTaskForRunnerConcurrentClaim(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
const numJobs = 3
|
||||
|
||||
run := &actions_model.ActionRun{
|
||||
Title: "concurrent-claim-test-run",
|
||||
RepoID: 1,
|
||||
OwnerID: 2,
|
||||
WorkflowID: "test.yaml",
|
||||
Index: 9901,
|
||||
TriggerUserID: 2,
|
||||
Ref: "refs/heads/main",
|
||||
CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0",
|
||||
Event: "push",
|
||||
TriggerEvent: "push",
|
||||
Status: actions_model.StatusWaiting,
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), run))
|
||||
|
||||
jobs := make([]*actions_model.ActionRunJob, numJobs)
|
||||
for i := range numJobs {
|
||||
jobID := "concurrent-job-" + string(rune('a'+i))
|
||||
jobs[i] = &actions_model.ActionRunJob{
|
||||
RunID: run.ID,
|
||||
RepoID: run.RepoID,
|
||||
OwnerID: run.OwnerID,
|
||||
CommitSHA: run.CommitSHA,
|
||||
Name: jobID,
|
||||
Attempt: 1,
|
||||
JobID: jobID,
|
||||
Status: actions_model.StatusWaiting,
|
||||
RunsOn: []string{"ubuntu-latest"},
|
||||
WorkflowPayload: minimalConcurrentWorkflowPayload(jobID),
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), jobs[i]))
|
||||
}
|
||||
|
||||
runners := make([]*actions_model.ActionRunner, numJobs)
|
||||
for i := range numJobs {
|
||||
r := &actions_model.ActionRunner{
|
||||
UUID: "concurrent-runner-uuid-" + string(rune('a'+i)),
|
||||
Name: "concurrent-runner-" + string(rune('a'+i)),
|
||||
AgentLabels: []string{"ubuntu-latest"},
|
||||
}
|
||||
r.GenerateAndFillToken()
|
||||
runners[i] = r
|
||||
require.NoError(t, db.Insert(t.Context(), runners[i]))
|
||||
}
|
||||
|
||||
// Simulate the burst: all runners call CreateTaskForRunner concurrently,
|
||||
// as happens when all see the same stale tasksVersion simultaneously.
|
||||
type result struct {
|
||||
task *actions_model.ActionTask
|
||||
ok bool
|
||||
err error
|
||||
}
|
||||
results := make([]result, numJobs)
|
||||
var wg sync.WaitGroup
|
||||
for i := range numJobs {
|
||||
wg.Go(func() {
|
||||
task, ok, err := actions_model.CreateTaskForRunner(t.Context(), runners[i])
|
||||
results[i] = result{task, ok, err}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Every runner must have received a task without error.
|
||||
claimedJobIDs := make(map[int64]bool)
|
||||
for i, r := range results {
|
||||
require.NoError(t, r.err, "runner %d got an unexpected error", i)
|
||||
require.True(t, r.ok, "runner %d did not get a task even though free jobs exist", i)
|
||||
require.NotNil(t, r.task)
|
||||
assert.False(t, claimedJobIDs[r.task.JobID], "job %d was claimed by more than one runner", r.task.JobID)
|
||||
claimedJobIDs[r.task.JobID] = true
|
||||
}
|
||||
assert.Len(t, claimedJobIDs, numJobs, "expected %d distinct jobs to be claimed", numJobs)
|
||||
|
||||
// All jobs must now be running with a task assigned.
|
||||
for _, j := range jobs {
|
||||
updated := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: j.ID})
|
||||
assert.Equal(t, actions_model.StatusRunning, updated.Status)
|
||||
assert.NotZero(t, updated.TaskID)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user