Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions source/FluxBase-Core-Tests/FBParallelExecutorTest.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,45 @@ FBParallelExecutorTest >> testAddPendingKeepsTheTokenTheTransitionWasQueuedWith
self assert: workItem token == token
]

{ #category : 'as yet unclassified' }
FBParallelExecutorTest >> testAFinishedScopeReadsItsOwnForkWhileAnotherPassRuns [
"A fork that two strands enter opens two scopes one after the other, and the body node
gets one activation per scope. Asked for its output, the first scope's executor looked
its fork up again and found the newest activation of that node - the running second
scope's, which is not done - so instead of answering the value of a closed scope it
answered the slot of its own last iteration. The same activation then said one thing
while the second pass ran and another once everything was over.

A scope's fork is its own: what a finished scope answers does not depend on whether
another pass through the same fork happens to be running"

| start split fork body reader flow instance readings after |
readings := OrderedCollection new.
start := FBStartEvent new.
split := start addOutgoingNode: ((FBPluggableTask named: 'split') block: [ :act |
act value: act argument ]).
fork := split addOutgoingNode: (FBParallelNode named: 'fork').
"a second transition into the same fork: the loop runs twice"
split addOutgoing: (FBTransition source: split target: fork).
body := fork addOutgoingNode: ((FBPluggableTask named: 'body') block: [ :act |
act value: act argument , '*' ]).
reader := body addOutgoingNode: ((FBPluggableTask named: 'reader') block: [ :act |
| firstBody |
firstBody := act flowInstance activations detect: [ :each | each node = body ].
"only what a scope other than the one that made it reads is at stake here"
firstBody executor == act executor ifFalse: [ readings add: firstBody output ].
act value: act argument ]).
reader addOutgoingNode: FBEndEvent new.
flow := FBFlow new start: start.
instance := flow newInstance.
instance executeWith: #( 'a' 'b' 'c' ).

self assert: instance state equals: #done.
after := (instance activations detect: [ :each | each node = body ]) output.
self assert: readings size equals: 3.
readings do: [ :each | self assert: each equals: after ]
]

{ #category : 'as yet unclassified' }
FBParallelExecutorTest >> testScopeBodyIsQueuedWithTheForksOwnChildToken [
"the body of a parallel scope is queued by the executor itself rather than by an
Expand Down
25 changes: 24 additions & 1 deletion source/FluxBase-Core/FBParallelExecutor.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ Class {
'iterationScope',
'iterationScopeIndex',
'openIterationScopes',
'current'
'current',
'forkActivation'
],
#category : 'FluxBase-Core-Model',
#package : 'FluxBase-Core',
Expand Down Expand Up @@ -39,6 +40,28 @@ FBParallelExecutor >> activateTransition: aTransition with: value token: aToken
self activate: activation
]

{ #category : 'accessing' }
FBParallelExecutor >> activationOf: aFBNode [
"my fork's activation is mine to keep, not to look up again. A node has one activation
per pass through the same fork, so the newest activation of my fork is only mine while
my scope is the one running: once my scope is over, that one is a later pass's.
Answering it made a finished scope read a running one's fork - #outputFor: asks
whether the loop is over, was told 'no' by another loop, and answered the slot of its
own last iteration instead of its first value. The same activation then said one thing
while the second pass ran and another once everything was over (reported 2026-09-18).

Resolved on first use rather than handed in at construction: at that moment - the
first step of my own scope, through #scopeToken - the newest activation of my fork is
mine, since a second pass through it cannot start before I am popped. An executor
persisted before this field existed resolves it the same way when it runs on.

Every other node is looked up as before, and for those the newest one is right: only a
live scope looks a node up, and its own activations are the newest there are"

aFBNode = node ifFalse: [ ^ super activationOf: aFBNode ].
^ forkActivation ifNil: [ forkActivation := super activationOf: aFBNode ]
]

{ #category : 'accessing' }
FBParallelExecutor >> activationClass [
^ node activationClass
Expand Down
Loading