Skip to content

Commit febc956

Browse files
data store: wipe workflow data on reload
* When a workflow is reloaded or restarted (node restart infers reload), there may be objects left behind in the store which have been wiped out in the scheduler by the configuration change. * To handle this, we must wipe all objects stored on the workflow (e.g. TaskProxies, TaskDefs, Jobs, etc), and rebuild from scratch. * Note, we don't need to wipe the data on the Workflow object itself (e.g. port, stateTotals, etc), these things will be refreshed by this or subsequent deltas.
1 parent beff755 commit febc956

File tree

10 files changed

+167
-6
lines changed

10 files changed

+167
-6
lines changed

src/services/treeCallback.js

+20-6
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,41 @@ class CylcTreeCallback extends DeltasCallback {
2525
}
2626
}
2727

28-
tearDown (store, errors) {
29-
// never tear down, this callback lives for the live of the UI
28+
before (deltas, store, errors) {
29+
// Wipe all child nodes from a workflow in the data store if a reloaded
30+
// delta is received. Reloaded deltas are sent whenever a workflow is
31+
// restarted or reloaded (note, restarting a workflow implicitly reloads
32+
// it).
33+
//
34+
// When a workflow reloads it is hard to generate the relevant pruned
35+
// and updated deltas to remove any objects which have been wiped out by
36+
// the configuration change, so the easiest solution is to wipe the
37+
// entire tree under the workflow and rebuild from scratch. If we don't
38+
// do this, we can end up with nodes in the store which aren't meant to be
39+
// there and won't get pruned.
40+
if (deltas.updated?.workflow?.reloaded) {
41+
store.commit('workflows/REMOVE_CHILDREN', (deltas.updated.workflow.id))
42+
}
43+
if (deltas.added?.workflow?.reloaded) {
44+
store.commit('workflows/REMOVE_CHILDREN', (deltas.added.workflow.id))
45+
}
3046
}
3147

3248
onAdded (added, store, errors) {
33-
// console.log('ADDED', added)
3449
store.commit('workflows/UPDATE_DELTAS', added)
3550
}
3651

3752
onUpdated (updated, store, errors) {
38-
// console.log('UPDATED', updated)
3953
store.commit('workflows/UPDATE_DELTAS', updated)
4054
}
4155

4256
onPruned (pruned, store, errors) {
4357
store.commit('workflows/REMOVE_DELTAS', pruned)
4458
}
4559

46-
// this callback does not need the before and commit methods
47-
before (a, b, c) {}
60+
// this callback does not need the tearDown and commit methods
4861
commit (a, b, c) {}
62+
tearDown (s, e) {}
4963
}
5064

5165
export default CylcTreeCallback

src/services/workflow.service.js

+1
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ class WorkflowService {
321321
const errors = []
322322

323323
// run the global callback first
324+
globalCallback.before(deltas, store, errors)
324325
globalCallback.onAdded(added, store, errors)
325326
globalCallback.onUpdated(updated, store, errors)
326327
globalCallback.onPruned(pruned, store, errors)

src/views/Dashboard.vue

+2
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ fragment UpdatedDelta on Updated {
180180
}
181181

182182
fragment WorkflowData on Workflow {
183+
# NOTE: do not request the "reloaded" event here
184+
# (it would cause a race condition with the workflow subscription)
183185
id
184186
status
185187
}

src/views/Graph.vue

+11
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ subscription Workflow ($workflowId: ID) {
127127
}
128128
}
129129

130+
fragment WorkflowData on Workflow {
131+
id
132+
reloaded
133+
}
134+
130135
fragment EdgeData on Edge {
131136
id
132137
source
@@ -154,6 +159,9 @@ fragment JobData on Job {
154159
}
155160

156161
fragment AddedDelta on Added {
162+
workflow {
163+
...WorkflowData
164+
}
157165
edges {
158166
...EdgeData
159167
}
@@ -166,6 +174,9 @@ fragment AddedDelta on Added {
166174
}
167175

168176
fragment UpdatedDelta on Updated {
177+
workflow {
178+
...WorkflowData
179+
}
169180
edges {
170181
...EdgeData
171182
}

src/views/README.md

+58
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,61 @@ E.G. the Tree *view* displays Cylc cycles/tasks/jobs in a collapsible hierarchy.
2525
It uses the Tree *component* which provides the generic tree logic e.g.
2626
indentation, expand/collapse, etc. This Tree *component* is also used by
2727
the Workflows *view*.
28+
29+
30+
## Subscriptions
31+
32+
Views typically register a subscription with the workflow service.
33+
34+
The workflow service when then issue this subscription and enter the data it
35+
returns into the global data store. The view can then access this data from the
36+
store.
37+
38+
The subscription must list all of the types (Workflow, TaskProxies, etc) and
39+
fields (name, status, etc) that the view requires.
40+
41+
In addition to this there are a number of rules that a subscription must match:
42+
43+
* The `id` field must be requested for every type requested.
44+
45+
This is used by the data store to process deltas.
46+
* You must request the corresponding `pruned` delta for each type you request.
47+
48+
This is used by the data store to remove objects when they drift out of the
49+
window.
50+
* You must request fields using "fragments" which must follow the standard
51+
naming pattern (e.g. `WorkflowData`, `CyclePointData`, etc).
52+
53+
This allows the subscription merging system to avoid requesting duplicate
54+
data.
55+
56+
When the subscription is updated, it gets printed to the console, you can
57+
use this to check merging has worked as intended.
58+
* If you want to walk the family tree (i.e. the hierarchy of families), then
59+
you must request the following fields of `FamilyProxies`:
60+
61+
* `__typename`
62+
* `ancestors { name }`
63+
* `childTasks { id }`
64+
* You must always request `workflow { reloaded }` if you request any of the
65+
following types:
66+
67+
* `Jobs`
68+
* `Tasks`
69+
* `Families`
70+
* `TaskProxies`
71+
* `FamilyProxies`
72+
* `Nodes`
73+
* `Edges`
74+
75+
The `reloaded` field is a special signal to the data store that tells it to
76+
wipe all objects of the above type within the workflow and rebuild from
77+
scratch. This happens when a workflow is reloaded or restarted to handle
78+
configuration chages (it's easier than trying to get the scheduler to
79+
send the appropriate updated and pruned deltas).
80+
81+
82+
## Example
83+
84+
For a minimal example view, see the SimpleTree. Note, this is only included
85+
in development builds.

src/views/SimpleTree.vue

+14
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ fragment Deltas on Deltas {
135135
}
136136

137137
fragment AddedDelta on Added {
138+
workflow {
139+
...WorkflowData
140+
}
138141
taskProxies {
139142
...TaskProxyData
140143
}
@@ -144,6 +147,9 @@ fragment AddedDelta on Added {
144147
}
145148

146149
fragment UpdatedDelta on Updated {
150+
workflow {
151+
...WorkflowData
152+
}
147153
taskProxies {
148154
...TaskProxyData
149155
}
@@ -160,6 +166,14 @@ fragment PrunedDelta on Pruned {
160166
jobs
161167
}
162168

169+
# We must always request the reloaded field whenever we are requesting things
170+
# within the workflow like tasks, cycles, etc as this is used to rebuild the
171+
# store when a workflow is reloaded or restarted.
172+
fragment WorkflowData on Workflow {
173+
id
174+
reloaded
175+
}
176+
163177
# We must always request the "id" for ALL types.
164178
# The only field this view requires beyond that is the status.
165179
fragment TaskProxyData on TaskProxy {

src/views/Table.vue

+1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ fragment PrunedDelta on Pruned {
100100

101101
fragment WorkflowData on Workflow {
102102
id
103+
reloaded
103104
}
104105

105106
fragment CyclePointData on FamilyProxy {

src/views/Tree.vue

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ fragment PrunedDelta on Pruned {
107107

108108
fragment WorkflowData on Workflow {
109109
id
110+
reloaded
110111
}
111112

112113
fragment CyclePointData on FamilyProxy {

src/views/Workflows.vue

+2
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ fragment UpdatedDelta on Updated {
6060
}
6161

6262
fragment WorkflowData on Workflow {
63+
# NOTE: do not request the "reloaded" event here
64+
# (it would cause a race condition with the workflow subscription)
6365
id
6466
status
6567
statusMsg

tests/unit/services/workflow.service.spec.js

+57
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
* along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
*/
1717

18+
import { createStore } from 'vuex'
19+
import storeOptions from '@/store/options'
20+
import CylcTreeCallback from '@/services/treeCallback'
1821
import sinon from 'sinon'
1922
import { print } from 'graphql/language'
2023
import gql from 'graphql-tag'
@@ -373,3 +376,57 @@ describe('WorkflowService', () => {
373376
})
374377
})
375378
})
379+
380+
describe('Global Callback', () => {
381+
it('should wipe workflow children on reloaded deltas', () => {
382+
// the callback should wipe workflow children when a "reloaded" delta is
383+
// received - see https://github.com/cylc/cylc-ui/pull/1479
384+
385+
// initiate the store
386+
const errors = {}
387+
const store = createStore(storeOptions)
388+
const callback = new CylcTreeCallback(store, errors)
389+
const cylcTree = store.state.workflows.cylcTree
390+
391+
// send an added delta which adds a workflow with one task
392+
const delta1 = {
393+
id: 123,
394+
added: {
395+
id: 123,
396+
workflow: { id: '~user/foo' },
397+
taskProxies: { id: '~user/foo//1/a' }
398+
}
399+
}
400+
callback.before(delta1, store, errors)
401+
callback.onAdded(delta1.added, store, errors)
402+
403+
// the user/workflow//cycle/task should now be in the store
404+
expect(Object.keys(cylcTree.$index)).to.deep.equal([
405+
'~user',
406+
'~user/foo',
407+
'~user/foo//1',
408+
'~user/foo//1/a',
409+
])
410+
411+
// send a reloaded delta which adds a new task
412+
const delta2 = {
413+
id: 234,
414+
added: {
415+
id: 234,
416+
workflow: { id: '~user/foo', reloaded: true },
417+
taskProxies: { id: '~user/foo//2/b' }
418+
}
419+
}
420+
callback.before(delta2, store, errors)
421+
callback.onUpdated(delta2.added, store, errors)
422+
423+
// the cycle "1" and task "1/a" should be gone from the store
424+
// without the need for an explicit "pruned" delta
425+
expect(Object.keys(cylcTree.$index)).to.deep.equal([
426+
'~user',
427+
'~user/foo',
428+
'~user/foo//2',
429+
'~user/foo//2/b',
430+
])
431+
})
432+
})

0 commit comments

Comments
 (0)