| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718 |
3x
3x
3x
3x
3x
3x
3x
221x
663x
344x
3x
117x
117x
117x
117x
3x
240x
3x
246x
3x
609x
609x
609x
609x
43x
566x
3x
504x
504x
504x
150x
2x
148x
354x
2332x
1985x
502x
502x
3x
495x
495x
495x
3x
492x
492x
492x
492x
492x
491x
491x
2324x
1833x
491x
492x
3x
195x
195x
3x
100x
32x
100x
3x
489x
489x
489x
489x
228x
261x
51x
51x
51x
210x
3x
490x
3x
490x
490x
490x
490x
490x
490x
490x
489x
489x
490x
3x
291x
291x
308x
291x
3x
36x
40x
3x
1x
1x
1x
3x
13x
13x
12x
1x
1x
1x
1x
13x
3x
883x
883x
154x
154x
729x
695x
500x
490x
490x
10x
10x
6x
4x
4x
4x
195x
195x
195x
195x
64x
729x
3x
468x
873x
873x
468x
3x
140x
196x
196x
140x
140x
3x
1x
1x
1x
1x
1x
1x
1x
3x
362x
362x
362x
362x
270x
362x
3x
4x
4x
4x
3x
358x
358x
358x
358x
358x
354x
354x
88x
266x
4x
4x
4x
4x
3x
139x
139x
139x
139x
139x
139x
139x
139x
3x
53x
53x
53x
53x
2x
2x
2x
51x
51x
51x
59x
59x
55x
59x
51x
3x
152x
152x
152x
152x
152x
152x
152x
152x
152x
213x
213x
213x
152x
| /*!
* Lifecycle Manager
*
* Copyright 2012 Antranig Basman
*
* Licensed under the New BSD license. You may not use this file except in
* compliance with this License.
*
* The research leading to these results has received funding from the European Union's
* Seventh Framework Programme (FP7/2007-2013)
* under grant agreement no. 289016.
*
* You may obtain a copy of the License at
* https://github.com/GPII/universal/blob/master/LICENSE.txt
*/
"use strict";
var fluid = fluid || require("infusion");
var gpii = fluid.registerNamespace("gpii");
(function () {
fluid.defaults("gpii.lifecycleManager", {
gradeNames: ["fluid.modelComponent", "fluid.contextAware"],
retryOptions: { // Options governing how often to recheck whether settings are set (and in future, to check for running processes)
rewriteEvery: 0, // This feature is now disabled except in integration tests, causing settings handler instability via GPII-2522
numRetries: 12, // Make 12 attempts over a period of 12 seconds to discover whether settings are set
retryInterval: 1000
},
components: {
variableResolver: {
type: "gpii.lifecycleManager.variableResolver"
},
nameResolver: {
type: "gpii.lifecycleManager.nameResolver"
}
},
dynamicComponents: {
sessions: {
type: "{arguments}.0", // a session grade derived from gpii.lifecycleManager.session
options: {
userToken: "{arguments}.1"
},
createOnEvent: "onSessionStart"
}
},
members: {
sessionIndex: {}, // map of userToken to session component member name, managed by gpii.lifecycleManager.sessionIndexer
/* queue for high-level lifecycle manager tasks (start, stop or update).
* The entries in the queue are of the format { func: <functionToCall>, arg: <argument> } where
* <functionToCall> is a single-argument function that returns a promise. The promise should be resolved when
* the function is complete (including side-effects).
* A more detailed description: https://github.com/GPII/universal/tree/master/documentation/LifecycleMananger.md
*/
queue: [],
isProcessingQueue: false // if queue is currently being processed
},
model: {
logonChange: {
type: undefined, // "login"/"logout"
inProgress: false, // boolean
userToken: undefined, // string with user token
timeStamp: 0
}
},
events: {
onSessionStart: null, // fired with [gradeName, userToken]
onSessionSnapshotUpdate: null, // fired with [{lifecycleManager}, {session}, originalSettings]
onSessionStop: null // fired with [{lifecycleManager}, {session}
},
listeners: {
"onSessionSnapshotUpdate.log": "gpii.lifecycleManager.logSnapshotUpdate",
"onCreate.createQueueFunctions": "gpii.lifecycleManager.createQueueFunctions"
},
queueFunctions: {
expander: { // manually expanded via the createQueueFunctions function
type: "fluid.noexpand",
value: {
stop: "{that}.processStop",
start: "{that}.processStart",
update: "{that}.processUpdate"
}
}
},
invokers: {
getActiveSessionTokens: {
funcName: "gpii.lifecycleManager.getActiveSessionTokens",
args: "{that}"
},
addLifecycleInstructionsToPayload: {
funcName: "gpii.lifecycleManager.addLifecycleInstructionsToPayload",
args: [ "{arguments}.0" ] // fullPayload
},
/** Accepts an array of user tokens, of which all but the first will currently be ignored. Returns the session
* component corresponding to that user, if they have an active session. A typical usage pattern is to call
* "getActiveSessionTokens" and send its return to "getSession"
*/
getSession: {
funcName: "gpii.lifecycleManager.getSession",
args: ["{that}", "{arguments}.0"] // user token
},
processQueue: {
funcName: "gpii.lifecycleManager.processQueue",
args: ["{that}", "{that}.queue"]
},
addToQueue: {
funcName: "gpii.lifecycleManager.addToQueue",
args: ["{that}", "{that}.queue", "{arguments}.0"]
},
// start: manually created function which should be called on user login (i.e. configuring the system)
// stop: manually created function which should be called on user logout (i.e. restoring the system)
// update: manually created function which should be called on update (i.e. changes in the setting of an already configured system)
processStart: { // should not be used directly, use the manually created 'start' invoker instead
funcName: "gpii.lifecycleManager.processStart",
args: [ "{that}", "{arguments}.0"]
},
processStop: { // should not be used directly, use the manually created 'stop' invoker instead
funcName: "gpii.lifecycleManager.processStop",
args: [ "{that}", "{arguments}.0"]
},
processUpdate: { // should not be used directly, use the manually created 'update' invoker instead
funcName: "gpii.lifecycleManager.processUpdate",
args: [ "{that}", "{arguments}.0"]
},
applySolution: {
funcName: "gpii.lifecycleManager.applySolution",
args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "{arguments}.3", "{arguments}.4"]
// solutionId, solutionRecord, session, lifecycleBlocksKeys, rootAction
},
executeActions: {
funcName: "gpii.lifecycleManager.executeActions",
args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "{arguments}.3", "{arguments}.4"]
// solutionId, settingsHandlers, actions, session, rootAction
},
invokeSettingsHandler: {
funcName: "gpii.lifecycleManager.invokeSettingsHandler",
args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"]
// solutionId, settingsHandlers, isMultiSH
},
restoreSnapshot: {
funcName: "gpii.lifecycleManager.restoreSnapshot",
args: ["{that}", "{arguments}.0"]
// originalSettings
}
}
});
fluid.defaults("gpii.test.lifecycleManager.integration", {
// Test this only in integration testing scenarios
retryOptions: {
rewriteEvery: 3
}
});
fluid.contextAware.makeAdaptation({
distributionName: "gpii.test.lifecycleManager.integration.adaptation",
targetName: "gpii.lifecycleManager",
adaptationName: "integrationTest",
checkName: "integrationTest",
record: {
contextValue: "{gpii.contexts.test.integration}",
gradeNames: "gpii.test.lifecycleManager.integration"
}
});
// Manually constructs the functions in the queueFunctions options.
// This done manually to avoid resolving the "{that}" part of the function
// to be called (e.g. "{that}.processStart")
gpii.lifecycleManager.createQueueFunctions = function (that) {
fluid.each(that.options.queueFunctions, function (queueFunc, invokerName) {
that[invokerName] = function (arg) {
return that.addToQueue({
func: queueFunc,
invokerName: invokerName,
arg: arg
});
};
});
};
gpii.lifecycleManager.addLifecycleInstructionsToPayload = function (fullPayload) {
fullPayload.activeConfiguration = {
inferredConfiguration: fluid.extend(true, {}, fullPayload.matchMakerOutput.inferredConfiguration[fullPayload.activeContextName])
};
var lifecycleInstructions = gpii.transformer.configurationToSettings(fullPayload.activeConfiguration.inferredConfiguration, fullPayload.solutionsRegistryEntries);
fluid.set(fullPayload, "activeConfiguration.lifecycleInstructions", lifecycleInstructions);
return fullPayload;
};
gpii.lifecycleManager.logSnapshotUpdate = function (lifecycleManager, session, originalSettings) {
fluid.log("Settings for session " + session.id + " created at " + session.createTime + " updated to ", originalSettings);
};
// Will return one of the user's token keys for an active session
// TODO: We need to implement logic to ensure at most one of these is set, or
// to manage logic for superposition of sessions if we permit several (see GPII-102)
gpii.lifecycleManager.getActiveSessionTokens = function (that) {
return fluid.keys(that.sessionIndex);
};
gpii.lifecycleManager.getSession = function (that, userTokens) {
userTokens = fluid.makeArray(userTokens);
Iif (userTokens.length === 0) {
fluid.fail("Attempt to get sessions without keys");
} else {
var memberKey = that.sessionIndex[userTokens[0]];
if (memberKey === undefined) {
return undefined;
}
return that[memberKey];
}
};
/** Transforms the handlerSpec (handler part of the transformer's response payload) to a form
* accepted by a settingsHandler - we use a 1-element array holding the payload for a single solution
* per handler
* @param isMultiSH {boolean} is a flag denoting whether it's a multi-settingshandler solution
* If it is, a supported settings block is required for each entry
*/
gpii.lifecycleManager.specToSettingsHandler = function (solutionId, handlerSpec, isMultiSH) {
var returnObj = {},
settings = {};
if (handlerSpec.supportedSettings === undefined) {
if (isMultiSH) {
fluid.fail("Solution " + solutionId + " has multiple settingshandlers but is missing " +
"the 'supportedSettings' directive. Will not set the settings for this settingshandler");
} else {
// if supportedSettings directive is not present, pass all settings:
settings = handlerSpec.settings;
}
} else {
// we cant simply use fluid.filterKeys because that wont handle the cases where
// there are 'undefined' values for the keys in handlerSpec.settings
// TODO: Kaspar believes that the reason for filtering happening here rather than in the MatchMaker is
// that the transformation of common terms into application specific settings doesn't occur until the
// transformation stage - so we don't have the full list of app-specific settings to filter until now.
for (var settingName in handlerSpec.supportedSettings) {
if (settingName in handlerSpec.settings) {
settings[settingName] = handlerSpec.settings[settingName];
}
}
}
returnObj[solutionId] = [{
settings: settings,
options: handlerSpec.options
}];
return returnObj; // NB array removed here
};
// Transform the response from the handler SET to a format that we can persist in models before passing to handler SET on restore
// - "oldValue" becomes {type: "ADD", value: <oldValue>}
// - `undefined` value becomes {type: "DELETE"}
gpii.lifecycleManager.responseToSnapshot = function (solutionId, handlerResponse) {
var unValued = gpii.settingsHandlers.setResponseToSnapshot(handlerResponse);
var armoured = gpii.settingsHandlers.settingsPayloadToChanges(unValued);
// Note - we deal in these 1-element arrays just for simplicity in the LifecycleManager. A more efficient
// implementation might send settings for multiple solutions to the same settingsHandler in a single request.
// Note that in the session's snapshots, this level of array containment has been removed.
return fluid.get(armoured, [solutionId, 0]);
};
/**
* @param handlerSpec {Object} A single settings handler specification
* @param isMultiSH {Boolean} [optional] if present and true, the solution has multiple settingshandlers
* Payload example:
* http://wiki.gpii.net/index.php/Settings_Handler_Payload_Examples
* Transformer output:
* http://wiki.gpii.net/index.php/Transformer_Payload_Examples
*/
gpii.lifecycleManager.invokeSettingsHandler = function (that, solutionId, handlerSpec, isMultiSH) {
// first prepare the payload for the settingsHandler in question - a more efficient
// implementation might bulk together payloads destined for the same handler
var settingsHandlerPayload = gpii.lifecycleManager.specToSettingsHandler(solutionId, handlerSpec, isMultiSH);
var resolvedName = that.nameResolver.resolveName(handlerSpec.type, "settingsHandler");
var setSettingsPromise = gpii.settingsHandlers.dispatchSettingsHandler(resolvedName, settingsHandlerPayload, that.options.retryOptions);
var togo = fluid.promise();
setSettingsPromise.then(function (handlerResponse) {
// update the settings section of our snapshot to contain the new information
var settingsSnapshot = gpii.lifecycleManager.responseToSnapshot(solutionId, handlerResponse);
// Settings handlers may or may not return options (currently it seems they all do) - gain resistance to this by restoring the
// original "options" supplied to them.
fluid.each(handlerSpec, function (entry, key) {
if (key !== "settings") {
settingsSnapshot[key] = fluid.copy(entry);
}
});
togo.resolve(settingsSnapshot);
}, togo.reject);
return togo;
};
gpii.lifecycleManager.invokeAction = function (action, nameResolver) {
var resolvedName = nameResolver.resolveName(action.type, "action");
return fluid.invokeGradedFunction(resolvedName, action);
};
/** Compensate for the effect of simpleminded merging when applied to a snapshot where an "DELETE" is merged on top of an "ADD"
*/
gpii.lifecycleManager.cleanDeletes = function (value) {
if (value.type === "DELETE") {
delete value.value;
}
return value;
};
/** Applies snapshotted settings from a single settingsHandler block attached to a single solution into the "originalSettings"
* model snapshot area in the LifecycleManager's session. Tightly bound to executeSettingsAction, executes one-to-one with it
* with almost identical argument list.
*/
gpii.lifecycleManager.recordSnapshotInSession = function (that, snapshot, solutionId, solutionRecord, session, settingsHandlerBlockName, rootAction) {
var toSnapshot = fluid.copy(solutionRecord);
toSnapshot.settingsHandlers = {};
toSnapshot.settingsHandlers[settingsHandlerBlockName] = snapshot;
if (rootAction === "start") {
session.applier.change(["originalSettings", solutionId], toSnapshot);
} else if (rootAction === "update") {
// if we're doing an update, keep the settings that are already stored from the
// original snapshot, but augment it with any settings from the new snapshot
// that were not present in the original snapshot.
// This workflow is tested in LifecycleManagerTests.js "Updating with normal reference to settingsHandler block, and 'undefined' value stored in snapshot"
var mergedSettings = fluid.extend(true, {}, toSnapshot, session.model.originalSettings[solutionId]);
var cleanedSettings = gpii.lifecycleManager.transformSolutionSettings(mergedSettings, gpii.lifecycleManager.cleanDeletes);
session.applier.change(["originalSettings", solutionId], cleanedSettings);
} else Eif (rootAction === "restore" || rootAction === "stop") {
// no-op - during a restore action we don't attempt to create a further snapshot
} else {
fluid.fail("Unrecognised rootAction " + rootAction);
}
};
/** In the case we are servicing a "restore" rootAction, wrap "dangerous" promises which perform system actions so
* that they do not reject, to ensure that we continue to try to restore the system come what may - GPII-2160
*/
gpii.lifecycleManager.wrapRestorePromise = function (promise, rootAction) {
return rootAction === "restore" && fluid.isPromise(promise) ?
gpii.rejectToLog(promise, " while restoring journal snapshot") : promise;
};
/**
* @param that {Object} The lifecycle manager component
* @param solutionId {String} the ID of the solution for which to execute the settings
* @param solutionRecord {Object} The solution registry entry for the solution
* @param session {Component} The current session component. This function will attach the
* solution record to the 'appliedSolutions' of the session's model (if successful)
* @param settingsHandlerBlockName {String} should be a reference to a settings block from the
* settingsHandlers section.
* @param rootAction {String} The root action on the LifecycleManager which is being serviced: "start", "stop",
* "update" or "restore"
* @return {Function} a nullary function (a task), that once executed will set the settings returning a promise
* that will be resolved once the settings are successfully set.
*/
gpii.lifecycleManager.executeSettingsAction = function (that, solutionId, solutionRecord, session,
settingsHandlerBlockName, rootAction) {
var isMultiSH = Object.keys(solutionRecord.settingsHandlers).length > 1;
var settingsHandlerBlock = solutionRecord.settingsHandlers[settingsHandlerBlockName];
Iif (settingsHandlerBlock === undefined) {
fluid.fail("Reference to non-existing settingsHandler block named " + settingsHandlerBlockName +
" in solution " + solutionId);
}
return function () {
var expanded = session.localResolver(settingsHandlerBlock);
var settingsPromise = that.invokeSettingsHandler(solutionId, expanded, isMultiSH);
settingsPromise.then(function (snapshot) {
session.applier.change(["appliedSolutions", solutionId], solutionRecord);
gpii.lifecycleManager.recordSnapshotInSession(that, snapshot, solutionId, solutionRecord, session,
settingsHandlerBlockName, rootAction);
});
return gpii.lifecycleManager.wrapRestorePromise(settingsPromise, rootAction);
};
};
/** For the complete entry for a single solution, transform each settingsHandler block by a supplied function - traditionally
* either gpii.settingsHandlers.changesToSettings or gpii.settingsHandlers.settingsToChanges .
* This is traditionally called during the "stop" action to unarmour all the settingsHandler blocks by converting from changes
* back to settings. In a future version of the SettingsHandler API, this will not be necessary.
* This is called during the "stop" action to convert the snapshotted "originalSettings" model material back to material
* suitable for being sent to executeSettingsAction, as well as at the corresponding point during the "journal restore"
* operation
* @param solutionSettings {Object} A settings block for a single solution, holding a member named `settingsHandlers`
* @param transformer {Function} A function which will transform one settingsHandlers block in the supplied `solutionSettings`
*/
gpii.lifecycleManager.transformSolutionSettings = function (solutionSettings, transformer) {
var togo = fluid.copy(solutionSettings); // safe since armoured
togo.settingsHandlers = fluid.transform(solutionSettings.settingsHandlers, function (handlerBlock) {
return gpii.settingsHandlers.transformOneSolutionSettings(handlerBlock, transformer);
});
return togo;
};
/** As for gpii.lifecycleManager.transformSolutionSettings, only transforms the complete collection of stored solutions
* (e.g. a value like session.model.originalSettings)
*/
gpii.lifecycleManager.transformAllSolutionSettings = function (allSettings, transformer) {
return fluid.transform(allSettings, function (solutionSettings) {
return gpii.lifecycleManager.transformSolutionSettings(solutionSettings, transformer);
});
};
/** Upgrades a promise rejection payload (or Error) by suffixing an additional "while" reason into its `message` field. If the
* payload is already an Error, its `message` field will be updated in place, otherwise a shallow clone of the original payload
* will be taken to perform the update.
* @param originError {Object|Error} A rejection payload. This should (at least) have the member `isError: true` set, as well
* as a String `message` holding a rejection reason.
* @param whileMsg {String} A message describing the activity which led to this error
* @return {Object} The rejected payload formed by shallow cloning the supplied argument (if it is not an `Error`) and
* suffixing its `message` member
*/
// TODO: Duplicate of kettle.upgradeError to avoid dependence on Kettle in this file. This needs to go into a new module once
// Kettle is factored up.
gpii.upgradeError = function (originError, whileMsg) {
var error = originError instanceof Error ? originError : fluid.extend({}, originError);
error.message = originError.message + whileMsg;
return error;
};
/** Transform a promise into one that always resolves, by intercepting its reject action and converting it to a logging action
* plus a resolve with an optionally supplied value.
* The error payload's message will be logged to `fluid.log` with the priority `fluid.logLevel.WARN`.
* @param promise {Promise} The promise to be transformed
* @param whileMsg {String} A suffix to be applied to the message, by the action of the utility `gpii.upgradeError`.
* This will typically begin with the text " while"
* @param resolveValue {Any} [optional] An optional value to be supplied to `resolve` of the returned promise, when the
* underlying promise rejects.
* @return {Promise} The wrapped promise which will resolve whether the supplied promise resolves or rejects.
*/
// TODO: In theory this is a fairly generic promise algorithm, but in practice there are some further subtleties - for
// example, a better system would instead replace fluid.promise.sequence with a version that allowed some form of
// "soft rejection" so that there might be a chance to signal failures to the user.
gpii.rejectToLog = function (promise, whileMsg, resolveValue) {
var togo = fluid.promise();
promise.then(function (value) {
togo.resolve(value);
}, function (error) {
gpii.upgradeError(error, whileMsg);
fluid.log(fluid.logLevel.WARN, error.message);
fluid.log(fluid.logLevel.WARN, error.stack);
togo.resolve(resolveValue);
});
return togo;
};
/** Called for each solution during "start", "stop" and "update" phases
* Actions to be performed are held in array "actions" and the settingsHandlers block from "solutions" (either Transformer
* output, or snapshot output from "start" phase) encodes the settings to be set.
* Returns the results from the settings action present in the list, and builds up action returns in session state
* "actionResults" field (so that these may be referenced from context expressions in further actions).
* @param that {Component:LifecycleManager} The LifecycleManager instance of type `gpii.lifecycleManager`
* @param solutionId {String} The id of the solution for which actions are to be performed
* @param solutionRecord {Object} Either the solution's registry entry (on start) or the fabricated "restore" entry (on stop)
* @param session {Component:LifecycleManagerSession} The LifecycleManager Session of type `gpii.lifecycleManager.session`
* @param actionBlock {String} The key for the particular block in the solutions registry entry which is being acted on - this
* may differ from `rootAction` because, e.g. an "update" action may be serviced by stopping and starting the solution, etc.
* @param rootAction {String} The root action on the LifecycleManager which is being serviced: "start", "stop", "update" or
* "restore"
* @return {Promise} A promise that will resolve when all the sequential actions have been performed. Any returned values
* will be built up as a side-effect within session.appliedSolutions, session.originalSettings, session.actionResults etc.
* If the `rootAction` is "restore", this promise will never reject, and instead the system will continue to make best
* efforts to continue executing actions even in the face of failures.
*/
gpii.lifecycleManager.executeActions = function (that, solutionId, solutionRecord, session, actionBlock, rootAction) {
var steps = solutionRecord[actionBlock];
if (steps === undefined) {
fluid.log("No " + actionBlock + " actions defined for solution " + solutionId);
return fluid.promise().resolve();
}
var sequence = fluid.transform(steps, function (action) {
if (typeof(action) === "string") {
// if the action is a reference to a settings block (settings.<refName> where <refName> is a key to
// the settings handler block)
if (action.indexOf("settings.") === 0) {
var settingsHandlerBlockName = action.substring("settings.".length);
return gpii.lifecycleManager.executeSettingsAction(that, solutionId, solutionRecord, session,
settingsHandlerBlockName, rootAction);
} else Eif (actionBlock === "update") {
// Keywords: "start", "stop", "configure" are allowed here as well, and
// and will result in evaluating the respective block
// TODO (GPII-1230) Fix this up so we don't always run the full start and stops (including)
// system restoration, etc.
if (action === "start" || action === "configure") {
return that.executeActions(solutionId, solutionRecord, session, action, rootAction);
} else Eif (action === "stop") {
// This branch is used when an "update" action requires a stop and start of the solution
var unarmoured = gpii.lifecycleManager.transformSolutionSettings(
session.model.originalSettings[solutionId], gpii.settingsHandlers.changesToSettings);
return that.executeActions(solutionId, unarmoured, session, "stop", rootAction);
} else {
fluid.fail("Unrecognised string action in LifecycleManager: " + action +
" inside 'update' section for solution " + solutionId);
}
}
} else { // TODO should be removed when GPII-1235 has been solved (lifecycle actions are settings handlers)
return function () {
var expanded = session.localResolver(action);
var result = gpii.lifecycleManager.invokeAction(expanded, that.nameResolver);
if (action.name) {
session.applier.change(["actionResults", action.name], result);
}
// TODO: It seems we have never supported asynchronous actions
};
}
});
return fluid.promise.sequence(sequence);
};
/** Invoked on "start", "update", "stop" and "restore" phases - in addition to forwarding to
* gpii.lifecycleManager.executeActions, it is responsible for saving the settings that are being set (when the fullSnapshot
* is true) and storing the list of applied solutions to the session state
*
* @param solutionId {String} the ID of the solution
* @param solutionRecord {Object} a solution record with settings that are to be applied to the system
* @param session {Object} the object holding the state of the system. This is updated in place by the settings application
* process, if `rootAction` is start.
* @param lifecycleBlockKeys {Array} Array of ordered strings denoting which lifecycle blocks to run (supported values here
* are "configure", "start" and/or "update")
* @param rootAction {String} Either "start", "update" or "stop" depending on the lifecycleManager phase which is executing
* @return {Promise} The same promise yielded by executeActions - the stateful construction of the session state is tacked
* onto this promise as a side-effect
*/
gpii.lifecycleManager.applySolution = function (that, solutionId, solutionRecord, session, lifecycleBlockKeys, rootAction) {
var promises = fluid.transform(lifecycleBlockKeys, function (key) {
// Courtesy to allow GPII-580 journalling tests to be expressed in-process - better expressed with FLUID-5790 cancellable promises
Eif (!fluid.isDestroyed(that)) {
return that.executeActions(solutionId, solutionRecord, session, key, rootAction);
}
});
return fluid.promise.sequence(promises);
};
/** Common utility used by gpii.lifecycleManager.stop and gpii.lifecycleManager.restoreSnapshot
* @param session {gpii.lifecycleManager.session} which must contain
* * A `originalSettings` snapshot in its model
* * A `localResolver` member for expanding material
* @param rootAction {String} Must be either "stop" or "restore"
* @return {Promise} A promise for the action of restoring the system
*/
gpii.lifecycleManager.restoreSystem = function (that, session, rootAction) {
var promises = fluid.transform(session.model.originalSettings, function (changesSolutionRecord, solutionId) {
var solutionRecord = gpii.lifecycleManager.transformSolutionSettings(changesSolutionRecord, gpii.settingsHandlers.changesToSettings);
return that.applySolution(solutionId, solutionRecord, session, [ "stop", "restore" ], rootAction);
});
// TODO: In theory we could stop all solutions in parallel
var sequence = fluid.promise.sequence(fluid.values(promises));
return sequence;
};
/** Restore a snapshot of settings, perhaps captured in the journal. This constructs a "fake" session using the special user token "restore"
* @param originalSettings {Object} The system snapshot to be restored
* @return A promise for the action of restoring the system
*/
gpii.lifecycleManager.restoreSnapshot = function (that, originalSettings) {
// TODO: document/ensure that this token, as well as the special "reset", is reserved
that.events.onSessionStart.fire("gpii.lifecycleManager.restoreSession", "restore");
var session = that[that.sessionIndex.restore];
session.applier.change("originalSettings", originalSettings);
var restorePromise = gpii.lifecycleManager.restoreSystem(that, session, "restore");
restorePromise.then(session.destroy, session.destroy);
return fluid.promise.map(restorePromise, function () {
return { // TODO: The standard response yield is unhelpful, consisting of the returns of any actions in "stop"
message: "The system's settings were restored from a snapshot",
payload: originalSettings
};
});
};
/**
* The lifecycleManager queue is used to hold the high-level actions that needs to happen,
* such as starting the login process, starting logout process, starting the update process.
* The entries in the queue are of the format { func: <unresolvedFunctionToCall>, invokerName: <invokerName>, arg: <argument> } where
* <unresolvedFunctionToCall> is the name of a single-argument function that returns a promise. The promise should be resolved when
* the function is complete (including side-effects). <InvokerName> is the name of the invoker that was called on the lifecycleManager
* for triggering this addToQueue function.
* The queue is run sequentially, and an item is considered "done" once the promise returned by its
* function is resolved.
*/
gpii.lifecycleManager.addToQueue = function (that, queue, item) {
var newItem = fluid.copy(item);
newItem.promise = fluid.promise();
queue.push(newItem);
if (!that.isProcessingQueue) {
that.processQueue();
}
return newItem.promise;
};
gpii.lifecycleManager.clearQueue = function (queue) {
while (queue.length > 0) {
var item = queue.shift();
item.promise.reject("clearQueue: clearing lifecycleManager promise from queue - remaining items: " + queue.length);
}
};
gpii.lifecycleManager.processQueue = function (that, queue) {
// mark that we're currently processing queue
that.isProcessingQueue = true;
// pick first item and process
var item = queue.shift();
var func = fluid.makeInvoker(that, {
func: item.func,
args: [ item.arg ]
}, "");
var promise = func();
promise.then(function (val) {
// resolve the original promise of the item;
item.promise.resolve(val);
// process next item in queue if it exists
if (queue.length > 0) {
that.processQueue(queue);
} else {
that.isProcessingQueue = false;
}
}, function (error) {
fluid.log(fluid.logLevel.ERROR, "An error occurred in an item of the lifecyclemanager's queue" +
"(invoker: " + item.invokerName + ", func: " + item.func + "), so clearing queue. Error was", error);
item.promise.reject();
gpii.lifecycleManager.clearQueue(queue);
that.isProcessingQueue = false;
});
};
/**
* Structure of lifecycleManager options:
* userid: userid,
* actions: either start or stop configuration from solutions registry
* settingsHandlers: transformed settings handler blocks
*/
gpii.lifecycleManager.processStop = function (that, options) {
var userToken = options.userToken;
var session = that.getSession([userToken]);
Iif (!session) {
var failPromise = fluid.promise();
failPromise.reject("No session was found when attempting keyout");
return failPromise;
}
var restorePromise = gpii.lifecycleManager.restoreSystem(that, session, "stop");
restorePromise.then(function () {
that.events.onSessionStop.fire(that, session);
session.destroy();
});
return restorePromise;
};
/**
* Update user preferences.
*/
gpii.lifecycleManager.processUpdate = function (that, finalPayload) {
var userToken = finalPayload.userToken,
lifecycleInstructions = finalPayload.activeConfiguration.lifecycleInstructions;
var session = that.getSession([userToken]);
if (!session) { // if user has logged out since the update was added to queue
var msg = "User with token " + userToken + " has no active session, so ignoring update request";
fluid.log(msg);
return fluid.promise().resolve(msg);
}
var appliedSolutions = session.model.appliedSolutions;
var promises = [];
fluid.each(lifecycleInstructions, function (solution, solutionId) {
var sol = fluid.copy(solution);
if (appliedSolutions[solutionId]) {
// merge already applied settings with the updates
sol = fluid.extend(true, {}, appliedSolutions[solutionId], sol);
}
promises.push(that.applySolution(solutionId, sol, session, [ "update" ], "update"));
});
return fluid.promise.sequence(promises);
};
gpii.lifecycleManager.processStart = function (that, finalPayload) {
var userToken = finalPayload.userToken,
lifecycleInstructions = finalPayload.activeConfiguration.lifecycleInstructions;
Iif (that.sessionIndex[userToken]) {
var failPromise = fluid.promise();
failPromise.reject("User already logged in when processing start item in queue. Aborting");
return failPromise;
}
that.events.onSessionStart.fire("gpii.lifecycleManager.userSession", userToken);
var session = that[that.sessionIndex[userToken]];
// TODO: Make global map of all users of session state
// activeConfiguration: Assigned in contextManager.updateActiveContextName, consumed in UserUpdate
// userToken, solutionsRegistryEntries: Assigned in initialPayload, consumed in UserUpdate
var filteredPayload = fluid.filterKeys(finalPayload, ["userToken", "preferences", "activeContextName", "activeConfiguration", "solutionsRegistryEntries", "matchMakerOutput"]);
session.applier.change("", filteredPayload); // TODO: One day after the applier is refactored this will explicitly need to be a "MERGE"
// This "start" action will result in the original settings of the system (i.e. those that
// were on the system before the user logged in) being stored inside session.model.originalSettings.
// When "stop" is called this payload will be used to restore the settings back to their
// original state.
var tasks = [];
fluid.each(lifecycleInstructions, function (solution, solutionId) {
tasks.push(function () {
Eif (!fluid.isDestroyed(that)) { // See above comment for GPII-580
// build structure for returned values (for later reset)
return that.applySolution(solutionId, solution, session,
(solution.active ? [ "configure", "start" ] : [ "configure" ]), "start");
}
});
});
// Note that these promises are only sequenced for their side-effects (the ones on session state within applySolution and on the system at large
// via the settings handlers)
return fluid.promise.sequence(tasks);;
};
})();
|