Skip to content

Commit d48e921

Browse files
committed
Move _disable_concurrency to static field to avoid unnecessary input lookups
1 parent 42dfac5 commit d48e921

4 files changed

Lines changed: 109 additions & 76 deletions

File tree

core/base.go

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ type NodeBaseInterface interface {
169169
IsExecutionNode() bool
170170
SetExecutionNode(execNode bool)
171171

172+
DisableConcurrency() bool
173+
SetDisableConcurrency(v bool)
174+
172175
// Returns the cache type where data is stored or should be stored to
173176
// By default this depends on if this is an execution node or not.
174177
GetCacheType() CacheType
@@ -183,9 +186,10 @@ type NodeBaseComponent struct {
183186
FullPath string // Full path of the node within the graph hierarchy
184187
CacheId string // Unique identifier for the cache
185188
NodeType string // Node type of the node (e.g. core/run@v1 or github.com/actions/checkout@v3)
186-
Graph *ActionGraph
187-
Parent NodeBaseInterface
188-
isExecutionNode bool
189+
Graph *ActionGraph
190+
Parent NodeBaseInterface
191+
isExecutionNode bool
192+
disableConcurrency bool
189193
}
190194

191195
func (n *NodeBaseComponent) GetCacheType() CacheType {
@@ -204,6 +208,14 @@ func (n *NodeBaseComponent) SetExecutionNode(execNode bool) {
204208
n.isExecutionNode = execNode
205209
}
206210

211+
func (n *NodeBaseComponent) DisableConcurrency() bool {
212+
return n.disableConcurrency
213+
}
214+
215+
func (n *NodeBaseComponent) SetDisableConcurrency(v bool) {
216+
n.disableConcurrency = v
217+
}
218+
207219
func (n *NodeBaseComponent) SetId(id string) {
208220
n.Id = id
209221
n.CacheId = fmt.Sprintf("%s:%s", n.Id, uuid.New().String())
@@ -562,21 +574,6 @@ func RegisterNodeFactory(nodeDefStr string, fn nodeFactoryFunc) error {
562574
nodeDef.Outputs[outputId] = outputDef
563575
}
564576

565-
// inject the hidden _disable_concurrency input for execution nodes.
566-
// When set to true at runtime, concurrent calls to this nodes
567-
// ExecuteImpl are serialized via a per-node-ID mutex
568-
if countExec > 0 {
569-
nodeDef.Inputs[InputId("_disable_concurrency")] = InputDefinition{
570-
PortDefinition: PortDefinition{
571-
Name: "Disable Concurrency",
572-
Type: "bool",
573-
Index: 999999,
574-
},
575-
HideSocket: true,
576-
Default: false,
577-
}
578-
}
579-
580577
id := fmt.Sprintf("%v@v%v", nodeDef.Id, nodeDef.Version)
581578
_, ok := registries[id]
582579
if ok {

core/executions.go

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -79,28 +79,24 @@ func (n *Executions) Execute(outputPort OutputId, ec *ExecutionState, err error)
7979
return nil
8080
}
8181

82-
// If the destination node has _disable_concurrency set to true, serialize execution
82+
// If the destination node has _disable_concurrency set, serialize execution
8383
// through a per-node-ID mutex to prevent concurrent ExecuteImpl calls.
8484
// The lock is stored as pending and released when the node calls Execute
8585
// to dispatch downstream (above), or as a fallback when ExecuteImpl
8686
// returns without dispatching (below).
87-
var dcNodeId string
88-
if nwi, ok := dest.DstNode.(NodeWithInputs); ok {
89-
dcVal, dcErr := InputValueById[bool](ec, nwi, InputId("_disable_concurrency"))
90-
if dcErr == nil && dcVal {
91-
dcNodeId = dest.DstNode.GetId()
92-
actual, _ := ec.Graph.ConcurrencyLocks.LoadOrStore(dcNodeId, &sync.Mutex{})
93-
mu := actual.(*sync.Mutex)
94-
mu.Lock()
95-
ec.PendingConcurrencyLocks.Store(dcNodeId, mu)
96-
// Fallback: release if ExecuteImpl returns without calling Execute
97-
// (end of chain, error, or panic).
98-
defer func() {
99-
if lockVal, loaded := ec.PendingConcurrencyLocks.LoadAndDelete(dcNodeId); loaded {
100-
lockVal.(*sync.Mutex).Unlock()
101-
}
102-
}()
103-
}
87+
if dest.DstNode.DisableConcurrency() {
88+
dcNodeId := dest.DstNode.GetId()
89+
actual, _ := ec.Graph.ConcurrencyLocks.LoadOrStore(dcNodeId, &sync.Mutex{})
90+
mu := actual.(*sync.Mutex)
91+
mu.Lock()
92+
ec.PendingConcurrencyLocks.Store(dcNodeId, mu)
93+
// Fallback: release if ExecuteImpl returns without calling Execute
94+
// (end of chain, error, or panic).
95+
defer func() {
96+
if lockVal, loaded := ec.PendingConcurrencyLocks.LoadAndDelete(dcNodeId); loaded {
97+
lockVal.(*sync.Mutex).Unlock()
98+
}
99+
}()
104100
}
105101

106102
err = dest.DstNode.ExecuteImpl(ec, dest.Port, err)

core/graph.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func (ag *ActionGraph) GetEntry() (NodeEntryInterface, error) {
8282

8383
func NewActionGraph() ActionGraph {
8484
return ActionGraph{
85-
Nodes: make(map[string]NodeBaseInterface),
85+
Nodes: make(map[string]NodeBaseInterface),
8686
ConcurrencyLocks: &sync.Map{},
8787
}
8888
}
@@ -250,10 +250,10 @@ func NewExecutionState(
250250
ctx, cancel := context.WithCancel(ctx)
251251

252252
return &ExecutionState{
253-
Graph: graph,
254-
Hierarchy: make([]NodeBaseInterface, 0),
255-
ContextStackLock: &sync.RWMutex{},
256-
OutputCacheLock: &sync.RWMutex{},
253+
Graph: graph,
254+
Hierarchy: make([]NodeBaseInterface, 0),
255+
ContextStackLock: &sync.RWMutex{},
256+
OutputCacheLock: &sync.RWMutex{},
257257
PendingConcurrencyLocks: &sync.Map{},
258258

259259
IsDebugSession: debugCb != nil,
@@ -821,6 +821,15 @@ func LoadInputValues(node NodeBaseInterface, nodeI map[string]any, validate bool
821821

822822
subInputs := map[string][]subInput{}
823823

824+
// _disable_concurrency is not a regular input, its stored directly on
825+
// the node instance so we pull it out before processing the rest.
826+
if v, ok := inputValues["_disable_concurrency"]; ok {
827+
if b, ok := v.(bool); ok && b {
828+
node.SetDisableConcurrency(true)
829+
}
830+
delete(inputValues, "_disable_concurrency")
831+
}
832+
824833
for portId, inputValue := range inputValues {
825834
groupInputId, portIndex, isIndexPort := IsValidIndexPortId(portId)
826835
if isIndexPort {

tests_unit/disable_concurrency_input_test.go

Lines changed: 66 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,47 +6,78 @@ import (
66
"testing"
77

88
"github.com/actionforge/actrun-cli/core"
9+
"go.yaml.in/yaml/v4"
910

1011
// initialize all nodes
1112
_ "github.com/actionforge/actrun-cli/nodes"
1213
)
1314

14-
func TestDisableConcurrencyInputInjection(t *testing.T) {
15-
registries := core.GetRegistries()
16-
if len(registries) == 0 {
17-
t.Fatal("no node types registered; node factories not loaded")
15+
func loadTestGraph(t *testing.T, graphStr string) core.ActionGraph {
16+
t.Helper()
17+
var graphYaml map[string]any
18+
if err := yaml.Unmarshal([]byte(graphStr), &graphYaml); err != nil {
19+
t.Fatalf("unmarshal YAML: %v", err)
1820
}
21+
ag, errs := core.LoadGraph(graphYaml, nil, "", false, core.RunOpts{})
22+
if len(errs) > 0 {
23+
t.Fatalf("LoadGraph: %v", errs[0])
24+
}
25+
return ag
26+
}
27+
28+
func TestDisableConcurrencyNotSetByDefault(t *testing.T) {
29+
ag := loadTestGraph(t, `
30+
entry: start
31+
nodes:
32+
- id: start
33+
type: core/start@v1
34+
position: {x: 0, y: 0}
35+
- id: run1
36+
type: core/run@v1
37+
position: {x: 100, y: 0}
38+
inputs:
39+
shell: bash
40+
script: echo hello
41+
connections: []
42+
executions:
43+
- src: {node: start, port: exec}
44+
dst: {node: run1, port: exec}
45+
`)
1946

20-
for id, nodeDef := range registries {
21-
// Count execution inputs to determine if this is an execution node.
22-
hasExecInput := false
23-
for _, inputDef := range nodeDef.Inputs {
24-
if inputDef.Exec {
25-
hasExecInput = true
26-
break
27-
}
28-
}
29-
30-
dcDef, hasDC := nodeDef.Inputs[core.InputId("_disable_concurrency")]
31-
32-
if hasExecInput {
33-
if !hasDC {
34-
t.Errorf("node %s has execution inputs but is missing _disable_concurrency input", id)
35-
continue
36-
}
37-
if dcDef.Type != "bool" {
38-
t.Errorf("node %s: _disable_concurrency type = %q, want \"bool\"", id, dcDef.Type)
39-
}
40-
if dcDef.Default != false {
41-
t.Errorf("node %s: _disable_concurrency default = %v, want false", id, dcDef.Default)
42-
}
43-
if !dcDef.HideSocket {
44-
t.Errorf("node %s: _disable_concurrency HideSocket = false, want true", id)
45-
}
46-
} else {
47-
if hasDC {
48-
t.Errorf("node %s has no execution inputs but has _disable_concurrency input", id)
49-
}
50-
}
47+
node := ag.Nodes["run1"]
48+
if node == nil {
49+
t.Fatal("node run1 not found")
50+
}
51+
if node.DisableConcurrency() {
52+
t.Error("expected DisableConcurrency to be false by default")
53+
}
54+
}
55+
56+
func TestDisableConcurrencySetFromYaml(t *testing.T) {
57+
ag := loadTestGraph(t, `
58+
entry: start
59+
nodes:
60+
- id: start
61+
type: core/start@v1
62+
position: {x: 0, y: 0}
63+
- id: run1
64+
type: core/run@v1
65+
position: {x: 100, y: 0}
66+
inputs:
67+
_disable_concurrency: true
68+
shell: bash
69+
script: echo hello
70+
connections: []
71+
executions:
72+
- src: {node: start, port: exec}
73+
dst: {node: run1, port: exec}
74+
`)
75+
76+
node := ag.Nodes["run1"]
77+
if node == nil {
78+
t.Fatal("node run1 not found")
79+
}
80+
if !node.DisableConcurrency() {
81+
t.Error("expected DisableConcurrency to be true when set in YAML")
5182
}
5283
}

0 commit comments

Comments
 (0)