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: 5 additions & 34 deletions source/FluxBase-Core-Tests/FBActivationTest.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -42,40 +42,6 @@ FBActivationTest >> testSourceNodeAnswersTheNodeTheActivationArrivedFrom [
self assert: arrivals asSet equals: (Set with: a with: b)
]

{ #category : 'as yet unclassified' }
FBActivationTest >> testSourceNodeFallsBackToTheLegacyParentIdWhenThereIsNoParentReference [
"Tokens persisted before FBToken>>parent became a real reference carry only the
parent's id. Rebuild that state directly - the setters for it are gone on purpose -
and check the old resolution path still answers the same node. Delete this together
with FBToken>>parentId once no such tokens are left in any store."
| start split a b join end flow instance arrival parentToken |
start := FBStartEvent new.
split := FBAndSplit named: 'split'.
a := FBTask named: 'a'.
b := FBTask named: 'b'.
join := FBAndJoin named: 'join'.
end := FBEndEvent new.
start --> split.
split --> a --> join.
split --> b --> join.
join --> end.
flow := FBFlow new start: start.
instance := flow newInstance.
instance execute.
arrival := (instance activations detect: [ :each | each node == join ]).
parentToken := arrival token parent.
arrival token
instVarNamed: 'parent' put: nil;
instVarNamed: 'parentId' put: parentToken id.
self assert: arrival token parent isNil.
self assert: arrival sourceNode equals: parentToken node
]

{ #category : 'as yet unclassified' }
FBActivationTest >> testSourceNodeIsNilWhenThereIsNeitherAParentNorALegacyId [
self assert: FBActivation new sourceNode isNil
]

{ #category : 'tests' }
FBActivationTest >> testEnvironment [
| activation node |
Expand Down Expand Up @@ -155,3 +121,8 @@ FBActivationTest >> testFinishedActivationPersistedWithoutATokenDoesNotReportIts
self assert: activation state equals: #done.
self assert: activation token state equals: #ready
]

{ #category : 'as yet unclassified' }
FBActivationTest >> testSourceNodeIsNilWithoutAParentToken [
self assert: FBActivation new sourceNode isNil
]
14 changes: 0 additions & 14 deletions source/FluxBase-Core-Tests/FBFlowExecutorTest.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -76,19 +76,6 @@ FBFlowExecutorTest >> testHasPendingReflectsPendingState [
self deny: instance executor hasPending
]

{ #category : 'as yet unclassified' }
FBFlowExecutorTest >> testIsCreatedToleratesRawTransitionPendingFromBeforeWorkItemMigration [
"a flow instance persisted before FBWorkItem existed can still have a raw
FBTransition sitting in pending after being loaded back into a newer image -
isCreated must not blow up on that shape, see FBTransition>>transition"
| instance realTransition |
instance := FBFlow simpleFlow newInstance.
instance initializeExecutor.
realTransition := instance start outgoing first.
instance executor instVarNamed: #pending put: (OrderedCollection with: realTransition).
self deny: instance executor isCreated
]

{ #category : 'as yet unclassified' }
FBFlowExecutorTest >> testIsDoneFalseWhilePendingOrSuspendedAreNotEmpty [
| instance |
Expand Down Expand Up @@ -214,4 +201,3 @@ FBFlowExecutorTest >> testTokenLineagePropagatesAcrossMultipleSequentialSteps [
self assert: endToken parent equals: taskToken.
self assert: ({ startToken id. taskToken id. endToken id } asSet size) equals: 3
]

16 changes: 0 additions & 16 deletions source/FluxBase-Core-Tests/FBFlowInstanceTest.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -287,22 +287,6 @@ FBFlowInstanceTest >> testNextTransitionForTheStartPseudoTransition [
self assert: flowInstance nextNode equals: flowInstance start
]

{ #category : 'tests' }
FBFlowInstanceTest >> testNextTransitionToleratesRawTransitionPendingFromBeforeWorkItemMigration [
"an instance persisted before FBWorkItem existed can still have a raw FBTransition
sitting in pending after being loaded back into a newer image. #nextTransition
unwraps its queue entry, and FBTransition>>transition is what keeps that readable"
| flowInstance realTransition |
flowInstance := FBFlow simpleFlow newInstance.
flowInstance initializeExecutor.
realTransition := flowInstance start outgoing first.
flowInstance executor
instVarNamed: #pending
put: (OrderedCollection with: realTransition).
self assert: flowInstance nextTransition == realTransition.
self assert: flowInstance nextNode equals: realTransition target
]

{ #category : 'tests' }
FBFlowInstanceTest >> testResumeWaitingAtContinuesTheFlow [
| flow instance waitNode |
Expand Down
21 changes: 0 additions & 21 deletions source/FluxBase-Core-Tests/FBParallelExecutorTest.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -161,27 +161,6 @@ FBParallelExecutorTest >> testEverySuspensionOfOneTransitionKeepsItsOwnToken [
self assert: tokens asSet size equals: 2
]

{ #category : 'as yet unclassified' }
FBParallelExecutorTest >> testTokenOfAnExecutorPersistedWithASingleTokenPerSuspensionIsStillFound [
"executors persisted while FBBaseExecutor>>addWaiting:withToken: kept one token per
suspended object hold that token itself where a queue of them is now expected, so
the reader has to tolerate both shapes - reaching into the ivar is the only way to
produce the old one"
| executor node token |
executor := FBParallelExecutor new
flowInstance: FBFlow simpleFlow newInstance;
node: (FBParallelNode named: 'p');
yourself.
node := FBTask named: 'waiting'.
token := FBToken new.
executor instVarNamed: 'suspendedTokens' put: (IdentityDictionary new
at: node put: token;
yourself).
self assert: (executor takeTokenForSuspended: node) == token.
"and it is forgotten, exactly as a token taken out of a queue is"
self assert: (executor takeTokenForSuspended: node) isNil
]

{ #category : 'tests' }
FBParallelExecutorTest >> testEachIterationRunsInItsOwnScope [
"an iteration is a lifetime: its tokens live in a scope of their own, opened per
Expand Down
78 changes: 78 additions & 0 deletions source/FluxBase-Core-Tests/FBQueueNormalizationTest.class.st
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"
Queues persisted in an older shape - a raw transition in #pending, a single token under a
suspension - are brought to the current one by FBFlowInstance>>normalizePersistedQueues,
which ApptiveGrid's database migration runs once. The old shapes are built by reaching
into the ivars, the only way left to produce them.
"
Class {
#name : 'FBQueueNormalizationTest',
#superclass : 'FBTest',
#category : 'FluxBase-Core-Tests',
#package : 'FluxBase-Core-Tests'
}

{ #category : 'tests' }
FBQueueNormalizationTest >> testARawPendingTransitionBecomesAWorkItem [
| instance transition |
instance := FBFlow simpleFlow newInstance.
instance initializeExecutor.
transition := instance start outgoing first.
instance executor instVarNamed: #pending put: (OrderedCollection with: transition).

self assert: instance normalizePersistedQueues.

self assert: (instance executor pending first isKindOf: FBWorkItem).
self assert: instance executor pending first transition identicalTo: transition.
self assert: instance executor pending first token isNil.
self assert: instance nextTransition identicalTo: transition.
instance initialValue: 7.
instance execute.
self assert: instance state equals: #done
]

{ #category : 'tests' }
FBQueueNormalizationTest >> testASingleSuspendedTokenBecomesAQueueOfOne [
| instance node token |
instance := FBFlow simpleFlow newInstance.
instance initializeExecutor.
node := FBTask named: 'waiting'.
token := FBToken new.
instance executor instVarNamed: 'suspendedTokens' put: (IdentityDictionary new at: node put: token; yourself).

self assert: instance normalizePersistedQueues.

self assert: ((instance executor instVarNamed: 'suspendedTokens') at: node) asArray equals: { token }.
self assert: (instance executor takeTokenForSuspended: node) identicalTo: token.
self assert: (instance executor takeTokenForSuspended: node) isNil
]

{ #category : 'tests' }
FBQueueNormalizationTest >> testTheExecutorsOfSubFlowInstancesAreNormalizedToo [
| parent child transition |
parent := FBFlow simpleFlow newInstance.
parent initializeExecutor.
child := FBFlow simpleFlow newInstance.
child initializeExecutor.
transition := child start outgoing first.
child executor instVarNamed: #pending put: (OrderedCollection with: transition).
parent subFlowInstances add: child.

self assert: parent normalizePersistedQueues.

self assert: (child executor pending first isKindOf: FBWorkItem).
self assert: child executor pending first transition identicalTo: transition
]

{ #category : 'tests' }
FBQueueNormalizationTest >> testAnUpToDateInstanceIsLeftAlone [
| instance before |
instance := FBFlow simpleWaitFlow newInstance.
instance executeWith: 4.
self assert: instance state equals: #waiting.
before := instance executor pending copy.

self deny: instance normalizePersistedQueues.

self assert: instance executor pending asArray equals: before asArray.
self deny: FBFlow simpleFlow newInstance normalizePersistedQueues
]
11 changes: 0 additions & 11 deletions source/FluxBase-Core-Tests/FBTokenTest.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,6 @@ FBTokenTest >> testTokenKnowsTheActivationItIsAttachedTo [
self assert: loose activation equals: activation
]

{ #category : 'as yet unclassified' }
FBTokenTest >> testLegacyParentIdReadsThroughToTheParentReference [
"#parentId only exists for tokens persisted before #parent was a real reference.
For a token that has a parent it must not contradict it."
| parent child |
parent := FBToken new.
child := FBToken new parent: parent; yourself.
self assert: child parentId equals: parent id.
self assert: FBToken new parentId isNil
]

{ #category : 'as yet unclassified' }
FBTokenTest >> testNewTokenHasReadyStateAndUniqueId [
| a b |
Expand Down
7 changes: 0 additions & 7 deletions source/FluxBase-Core-Tests/FBTransitionTest.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,3 @@ Class {
#category : 'FluxBase-Core-Tests',
#package : 'FluxBase-Core-Tests'
}

{ #category : 'as yet unclassified' }
FBTransitionTest >> testTransitionAnswersItselfForDuckTypingWithWorkItem [
| transition |
transition := FBTransition new.
self assert: transition transition == transition
]
17 changes: 1 addition & 16 deletions source/FluxBase-Core/FBBaseActivation.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -255,22 +255,7 @@ FBBaseActivation >> sourceNode [
taken to produce me. Not derivable from the graph - my node may have several
incoming transitions, and a loop passes the same one repeatedly - so it comes from
my token's parent, which is the token of the activation that spawned me."
^ self token parent
ifNotNil: [ :parentToken | parentToken node ]
ifNil: [ self legacySourceNode ]
]

{ #category : 'accessing' }
FBBaseActivation >> legacySourceNode [
"Tokens persisted before FBToken held a real #parent reference only carry the
parent's id, so resolve it the way FBJoinBehavior used to: search this instance's
activations for the one owning that token. Remove together with FBToken>>parentId
once no such tokens are left in any store."
| parentTokenId |
parentTokenId := self token parentId ifNil: [ ^ nil ].
^ (self instance activations
detect: [ :each | each token id = parentTokenId ]
ifNone: [ nil ]) ifNotNil: [ :each | each node ]
^ self token parent ifNotNil: [ :parentToken | parentToken node ]
]

{ #category : 'as yet unclassified' }
Expand Down
72 changes: 39 additions & 33 deletions source/FluxBase-Core/FBBaseExecutor.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,8 @@ FBBaseExecutor >> tokenForSuspended: anObject satisfying: aBlock [
"the first token stored for anObject that aBlock accepts, left where it is - the one
#takeTokenForSuspended:satisfying: would take. For a look before the taking
(#resume:withToken:value:)"
| tokens |
suspendedTokens ifNil: [ ^ nil ].
tokens := suspendedTokens at: anObject ifAbsent: [ ^ nil ].
(tokens isKindOf: Collection) ifFalse: [ ^ (aBlock value: tokens) ifTrue: [ tokens ] ifFalse: [ nil ] ].
^ tokens detect: aBlock ifNone: [ nil ]
^ (suspendedTokens at: anObject ifAbsent: [ ^ nil ]) detect: aBlock ifNone: [ nil ]
]

{ #category : 'accessing' }
Expand All @@ -267,12 +264,6 @@ FBBaseExecutor >> takeTokenForSuspended: anObject satisfying: aBlock [
| tokens token |
suspendedTokens ifNil: [ ^ nil ].
tokens := suspendedTokens at: anObject ifAbsent: [ ^ nil ].
"executors persisted while one token was kept per suspended object hold that token
itself rather than a queue of them"
(tokens isKindOf: Collection) ifFalse: [
(aBlock value: tokens) ifFalse: [ ^ nil ].
suspendedTokens removeKey: anObject.
^ tokens ].
token := tokens detect: aBlock ifNone: [ ^ nil ].
tokens remove: token.
tokens ifEmpty: [ suspendedTokens removeKey: anObject ].
Expand Down Expand Up @@ -405,17 +396,40 @@ FBBaseExecutor >> suspendedExecutor: anObject [
suspendedExecutor := anObject
]

{ #category : 'migrating' }
FBBaseExecutor >> normalizeQueues [
"bring my queues from the shapes older stores wrote to the current one, and answer
whether anything changed: a raw FBTransition in #pending (from before FBWorkItem,
2026-09-03) becomes a work item without a token, and a single token under a
suspension in #suspendedTokens (from before one suspension could hold several) becomes
a queue of one. What FBFlowInstance>>normalizePersistedQueues asks of every executor
for ApptiveGrid's database migration; nothing else reads the old shapes any more"
| changed |
changed := false.
pending withIndexDo: [ :each :i |
(each isKindOf: FBWorkItem) ifFalse: [
pending at: i put: (FBWorkItem new transition: each; yourself).
changed := true ] ].
suspendedTokens ifNotNil: [
suspendedTokens keys do: [ :key |
| tokens |
tokens := suspendedTokens at: key.
(tokens isKindOf: Collection) ifFalse: [
suspendedTokens at: key put: (OrderedCollection with: tokens).
changed := true ] ] ].
^ changed
]

{ #category : 'testing' }
FBBaseExecutor >> hasTokenIn: aScope [
"is a token of aScope, or of a scope nested in it, still with me - queued on a work
item or parked for a suspension? What FBFlowInstance>>isScopeLive: asks every active
executor. A pending entry from before FBWorkItem existed carries no token"
(pending anySatisfy: [ :each | aScope encloses: each token scope ]) ifTrue: [ ^ true ].
executor. A work item queued without a token (a rerun, a normalized old entry)
belongs to no scope"
(pending anySatisfy: [ :each | each token notNil and: [ aScope encloses: each token scope ] ]) ifTrue: [ ^ true ].
suspendedTokens ifNil: [ ^ false ].
^ suspendedTokens values anySatisfy: [ :tokens |
(tokens isKindOf: Collection)
ifTrue: [ tokens anySatisfy: [ :token | aScope encloses: token scope ] ]
ifFalse: [ aScope encloses: tokens scope ] ]
tokens anySatisfy: [ :token | aScope encloses: token scope ] ]
]

{ #category : 'cancelling' }
Expand All @@ -424,10 +438,10 @@ FBBaseExecutor >> withdrawTokensSatisfying: aBlock [
tokens taken: work items off #pending, and tokens off the queues in #suspendedTokens
- a suspension whose last token went falls out of #suspended with it, once per
token taken, since #suspended holds the same object once per parked strand. A
suspension that recorded no token (parked before tokens were kept), and a pending
entry from before work items existed, are never touched. The one place the queues
are emptied from outside a step; FBFlowInstance>>withdrawStrandsSatisfying: asks it
of every active executor and settles me afterwards"
suspension that recorded no token (parked before tokens were kept), and a work item
queued without one, are never touched. The one place the queues are emptied from
outside a step; FBFlowInstance>>withdrawStrandsSatisfying: asks it of every active
executor and settles me afterwards"

| taken |
taken := OrderedCollection new.
Expand All @@ -438,20 +452,12 @@ FBBaseExecutor >> withdrawTokensSatisfying: aBlock [
suspendedTokens keys do: [ :suspension |
| queue |
queue := suspendedTokens at: suspension.
(queue isKindOf: Collection)
ifTrue: [
(queue select: aBlock) do: [ :token |
queue remove: token.
suspended remove: suspension ifAbsent: [ ].
suspension strandWithdrawn: token.
taken add: token ].
queue ifEmpty: [ suspendedTokens removeKey: suspension ] ]
ifFalse: [
(aBlock value: queue) ifTrue: [
suspendedTokens removeKey: suspension.
suspended remove: suspension ifAbsent: [ ].
suspension strandWithdrawn: queue.
taken add: queue ] ] ].
(queue select: aBlock) do: [ :token |
queue remove: token.
suspended remove: suspension ifAbsent: [ ].
suspension strandWithdrawn: token.
taken add: token ].
queue ifEmpty: [ suspendedTokens removeKey: suspension ] ].
^ taken
]

Expand Down
Loading
Loading