mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-08-02 12:53:08 +00:00
fix: fix panics, enable the forcetypeassert lint (#1123)
An unchecked type assertion panics on input it did not expect, as a missing `tool_cache` key did in https://gitea.com/gitea/runner/pulls/1122.
Every flagged site is now handled where it can fail, or typed so it cannot: a `lock.Keyed` replaces the two `sync.Map` mutex registries, and the reporter's outputs carry an explicit sent flag. Mocks keep their assertions, a mismatch there is a setup error the panic names.
Bugs it turned up (only the first is reachable from workflows):
1. A scalar `matrix.include` or `matrix.exclude`, e.g. `include: foo` or `include: [1, 2]`, panicked the runner with `interface conversion: interface {} is string, not map[string]interface {}`. Verified against `main`, it is now a workflow error. `OnSchedule` panicked the same way on a malformed `on.schedule` entry.
1. `ExternalURL()` panicked on the nil listener after `Close()`, the port is now resolved once at startup.
1. `errors.Is(err, git.ErrShortRef)` followed by `err.(*git.Error)` panics as soon as anything wraps that error, so it is `errors.As` now.
1. An output name the server acknowledged without ever being sent one was recorded as sent forever, which silently dropped a later value for that name.
48576ab3e5 fixes one discovered issue: a matrix key holding a nested object was logged and then run as if the job had no matrix, so it now fails like an unknown `exclude` key.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1123
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
@@ -32,6 +32,12 @@ const (
|
||||
maxOutputValueLen = 1024 * 1024 // 1 MiB
|
||||
)
|
||||
|
||||
// jobOutput is a job output on its way to the server, sent once the server has acknowledged it.
|
||||
type jobOutput struct {
|
||||
value string
|
||||
sent bool
|
||||
}
|
||||
|
||||
type Reporter struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
@@ -53,7 +59,8 @@ type Reporter struct {
|
||||
state *runnerv1.TaskState
|
||||
stateChanged bool
|
||||
stateMu sync.RWMutex
|
||||
outputs sync.Map
|
||||
outputsMu sync.Mutex
|
||||
outputs map[string]jobOutput
|
||||
daemon chan struct{}
|
||||
heartbeatStop chan struct{}
|
||||
heartbeatStopOnce sync.Once
|
||||
@@ -394,7 +401,12 @@ func (r *Reporter) logf(format string, a ...any) {
|
||||
func (r *Reporter) SetOutputs(outputs map[string]string) {
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
r.outputsMu.Lock()
|
||||
defer r.outputsMu.Unlock()
|
||||
|
||||
if r.outputs == nil {
|
||||
r.outputs = map[string]jobOutput{}
|
||||
}
|
||||
for k, v := range outputs {
|
||||
if l := len(k); l > maxOutputKeyLen {
|
||||
log.Warnf("ignore output %q because the key is too long: %d > %d", k, l, maxOutputKeyLen)
|
||||
@@ -406,10 +418,9 @@ func (r *Reporter) SetOutputs(outputs map[string]string) {
|
||||
r.logf("ignore output %q because the value is too long: %d > %d", k, l, maxOutputValueLen)
|
||||
continue
|
||||
}
|
||||
if _, ok := r.outputs.Load(k); ok {
|
||||
continue
|
||||
if _, ok := r.outputs[k]; !ok {
|
||||
r.outputs[k] = jobOutput{value: v}
|
||||
}
|
||||
r.outputs.Store(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -574,14 +585,14 @@ func (r *Reporter) ReportState(reportResult bool) error {
|
||||
r.clientM.Lock()
|
||||
defer r.clientM.Unlock()
|
||||
|
||||
// Build the outputs map first (single Range pass instead of two).
|
||||
outputs := make(map[string]string)
|
||||
r.outputs.Range(func(k, v any) bool {
|
||||
if val, ok := v.(string); ok {
|
||||
outputs[k.(string)] = val
|
||||
r.outputsMu.Lock()
|
||||
for key, out := range r.outputs {
|
||||
if !out.sent {
|
||||
outputs[key] = out.value
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
r.outputsMu.Unlock()
|
||||
|
||||
// Consume stateChanged atomically with the snapshot; restored on error
|
||||
// below so a concurrent Fire() during UpdateTask isn't silently lost.
|
||||
@@ -594,7 +605,8 @@ func (r *Reporter) ReportState(reportResult bool) error {
|
||||
r.stateMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
state := proto.Clone(r.state).(*runnerv1.TaskState)
|
||||
state := &runnerv1.TaskState{}
|
||||
proto.Merge(state, r.state)
|
||||
r.stateChanged = false
|
||||
r.stateMu.Unlock()
|
||||
|
||||
@@ -622,21 +634,23 @@ func (r *Reporter) ReportState(reportResult bool) error {
|
||||
metrics.ReportStateTotal.WithLabelValues(metrics.LabelResultSuccess).Inc()
|
||||
r.lastReportedAtNanos.Store(time.Now().UnixNano())
|
||||
|
||||
var noSent []string
|
||||
r.outputsMu.Lock()
|
||||
for _, k := range resp.Msg.SentOutputs {
|
||||
r.outputs.Store(k, struct{}{})
|
||||
if _, ok := r.outputs[k]; ok {
|
||||
r.outputs[k] = jobOutput{sent: true}
|
||||
}
|
||||
}
|
||||
for key, out := range r.outputs {
|
||||
if !out.sent {
|
||||
noSent = append(noSent, key)
|
||||
}
|
||||
}
|
||||
r.outputsMu.Unlock()
|
||||
|
||||
if resp.Msg.State != nil && resp.Msg.State.Result == runnerv1.Result_RESULT_CANCELLED {
|
||||
r.cancel()
|
||||
}
|
||||
|
||||
var noSent []string
|
||||
r.outputs.Range(func(k, v any) bool {
|
||||
if _, ok := v.(string); ok {
|
||||
noSent = append(noSent, k.(string))
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(noSent) > 0 {
|
||||
return fmt.Errorf("there are still outputs that have not been sent: %v", noSent)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
@@ -1011,33 +1013,59 @@ func TestReporter_SetOutputs(t *testing.T) {
|
||||
r := &Reporter{state: &runnerv1.TaskState{}}
|
||||
|
||||
r.SetOutputs(map[string]string{"foo": "bar"})
|
||||
got, ok := r.outputs.Load("foo")
|
||||
got, ok := r.outputs["foo"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "bar", got)
|
||||
assert.Equal(t, "bar", got.value)
|
||||
|
||||
// first value wins: a later write to the same key is ignored
|
||||
r.SetOutputs(map[string]string{"foo": "baz"})
|
||||
got, _ = r.outputs.Load("foo")
|
||||
assert.Equal(t, "bar", got)
|
||||
assert.Equal(t, "bar", r.outputs["foo"].value)
|
||||
|
||||
// keys longer than maxOutputKeyLen are dropped
|
||||
longKey := strings.Repeat("k", maxOutputKeyLen+1)
|
||||
r.SetOutputs(map[string]string{longKey: "v"})
|
||||
_, ok = r.outputs.Load(longKey)
|
||||
_, ok = r.outputs[longKey]
|
||||
assert.False(t, ok)
|
||||
|
||||
// values longer than maxOutputValueLen are dropped
|
||||
longValue := strings.Repeat("v", maxOutputValueLen+1)
|
||||
r.SetOutputs(map[string]string{"big": longValue})
|
||||
_, ok = r.outputs.Load("big")
|
||||
_, ok = r.outputs["big"]
|
||||
assert.False(t, ok)
|
||||
|
||||
// a value at exactly the limit is still stored
|
||||
maxValue := strings.Repeat("v", maxOutputValueLen)
|
||||
r.SetOutputs(map[string]string{"atlimit": maxValue})
|
||||
got, ok = r.outputs.Load("atlimit")
|
||||
got, ok = r.outputs["atlimit"]
|
||||
require.True(t, ok)
|
||||
assert.Len(t, got, maxOutputValueLen)
|
||||
assert.Len(t, got.value, maxOutputValueLen)
|
||||
}
|
||||
|
||||
// An output the server acknowledged is not reported again.
|
||||
func TestReporter_OutputsSentOnce(t *testing.T) {
|
||||
client := mocks.NewClient(t)
|
||||
var reported []map[string]string
|
||||
client.On("UpdateTask", mock.Anything, mock.Anything).Return(
|
||||
func(_ context.Context, req *connect_go.Request[runnerv1.UpdateTaskRequest]) (*connect_go.Response[runnerv1.UpdateTaskResponse], error) {
|
||||
reported = append(reported, req.Msg.Outputs)
|
||||
return connect_go.NewResponse(&runnerv1.UpdateTaskResponse{SentOutputs: slices.Collect(maps.Keys(req.Msg.Outputs))}), nil
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||
require.NoError(t, err)
|
||||
cfg, _ := config.LoadDefault("")
|
||||
r := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg)
|
||||
|
||||
r.SetOutputs(map[string]string{"foo": "bar"})
|
||||
require.NoError(t, r.ReportState(false))
|
||||
assert.True(t, r.outputs["foo"].sent)
|
||||
|
||||
require.NoError(t, r.ReportState(true))
|
||||
require.Len(t, reported, 2)
|
||||
assert.Equal(t, map[string]string{"foo": "bar"}, reported[0])
|
||||
assert.Empty(t, reported[1])
|
||||
}
|
||||
|
||||
func TestReporter_EffectiveCloseTimeout(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user