-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclass_Upgradable.erl
More file actions
950 lines (658 loc) · 31.7 KB
/
class_Upgradable.erl
File metadata and controls
950 lines (658 loc) · 31.7 KB
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
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
% Copyright (C) 2022-2026 Olivier Boudeville
%
% This file is part of the Ceylan-WOOPER library.
%
% This library is free software: you can redistribute it and/or modify
% it under the terms of the GNU Lesser General Public License or
% the GNU General Public License, as they are published by the Free Software
% Foundation, either version 3 of these Licenses, or (at your option)
% any later version.
% You can also redistribute it and/or modify it under the terms of the
% Mozilla Public License, version 1.1 or later.
%
% This library is distributed in the hope that it will be useful,
% but WITHOUT ANY WARRANTY; without even the implied warranty of
% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
% GNU Lesser General Public License and the GNU General Public License
% for more details.
%
% You should have received a copy of the GNU Lesser General Public
% License, of the GNU General Public License and of the Mozilla Public License
% along with this library.
% If not, see <http://www.gnu.org/licenses/> and
% <http://www.mozilla.org/MPL/>.
%
% Author: Olivier Boudeville [olivier (dot) boudeville (at) esperide (dot) com]
% Creation date: Friday, August 12, 2022.
-module(class_Upgradable).
-moduledoc """
Interface class implementing the **Upgradable** trait, so that instances
supporting that trait are able to **be hot-updated**, that is to have their
class definition changed at runtime (either upgraded or downgraded), with no
need to restart the system as a whole.
So the objective is that the instances implementing this interface do not have
to terminate, and may update themselves on the fly (once their new class has
been loaded), code-wise and also state-wise.
For that, a concreate Upgradable child class should (besides inheriting from
this interface):
- implement a relevant get_version/1 static method, whose signature is: `-spec
get_version() -> static_return(any_version()).`
- possibly override its {up,down}gradeVersion/4 member methods
See also class_Upgradable_test.erl and the support of the
'freezeUntilVersionChange' special message in the WOOPER main loop (refer to
wooper_main_loop_functions.hrl).
As by default no WOOPER-level instance tracking is performed (this is often an
application-specific topic), the PIDs of the instances to update have to be
provided by the caller.
Should instances be left over (i.e. not be updated), depending on the
preferences specified when triggering the update, either this update will be
reported as failed, or these instances will be killed - not at the first missed
upgrade (as they will just linger then in old code), but at the next one.
To avoid unwanted calls to be processed during an update, the relevant
processes, notably instances, shall be frozen. When a class is updated, this
includes not only its direct instances but also the one of all classes deriving
from it.
A difficulty is that by default nothing prevents static methods / functions
exported by this class to be called just before said update and to interfere /
have their possibly mostly unrelated process be killed. Determining the culprits
and freezing them until none of them gets in the way of a soft purge might be a
good solution.
A question is how the PIDs of the instances of an updated class are to be
determined (see
<https://erlangforums.com/t/determining-processes-lingering-in-old-code/1755>
for a related discussion).
Either each class keeps track of its instances (not recommended, as incurs
systematic overhead and may not be scalable), or a massive scan is performed
(then preferably in a concurrent way, as done by the ERTS code purger; see
do_soft_purge/2 in erts_code_purger.erl for that).
""".
-define( class_description,
"Interface implementing the Upgradable trait, for all instances able "
"to be upgraded/downgraded on the fly, at runtime, with no specific "
"restarting." ).
% Emitting traces in useful, but cannot be class_Traceable as we are at the
% level of WOOPER:
%
-define( superclasses, [] ).
-define( class_attributes, [
% Previously, for a better controllability, this information was at
% instance-level, but a static information is fully sufficient:
%
%{ wooper_upgradable_version, any_version(),
% "the current version of this class" }
] ).
-doc "The PID of an instance implementing the Upgradable interface.".
-type upgradable_pid() :: pid().
-doc """
Any extra data (akin to release-specific information) of use when performing a
version change.
""".
-type extra_data() :: any().
% Note: for messages, we cannot describe values like [V1, V2] as being of type
% [T1(), T2()], so we use [T1() | T2()] instead.
-doc """
Information about an instance after it applied a freeze request.
""".
-type freeze_info() :: { classname(), instance_pid() }.
-doc """
Message sent back to the sender of a 'freezeUntilVersionChange' special message.
This message may be interpreted as a oneway call.
""".
-type freeze_notification() :: { 'onInstanceFrozen', freeze_info() }.
-doc """
A report sent by an instance that succeeded in updating itself to the specified
version.
""".
-type update_success_report() :: { classname(), instance_pid(), any_version() }.
-doc """
A report sent by an instance that failed in updating itself and ended up in the
specified version (most probably its pre-update one).
""".
-type update_failure_report() ::
{ error_reason(), classname(), instance_pid(), any_version() }.
-doc """
Outcome of an instance update, which may be an upgrade or a downgrade.
This is a message (possibly interpreted as a oneway call) sent back to the
caller by an instance having being requested to update.
""".
-type update_outcome() :: { 'onUpdateSuccess', update_success_report() }
| { 'onUpdateFailure', update_failure_report() }.
-export_type([ upgradable_pid/0, extra_data/0,
freeze_notification/0,
update_success_report/0, update_failure_report/0,
update_outcome/0 ]).
% Exported helper functions that can be applied to Upgradable instances (only):
-export([ manage_version_change/4, get_version/1, to_string/1 ]).
% Exported helper functions that can be applied to any WOOPER state:
-export([ is_upgradable/1, get_maybe_version/1, to_maybe_string/1 ]).
% Allows to define WOOPER base variables and methods for that class:
-include_lib("wooper/include/wooper.hrl").
% Must be included before the class_TraceEmitter header:
-define( trace_emitter_categorization, "Upgradable" ).
% Would allow to use macros for trace sending, yet we are in WOOPER here:
%-include_lib("traces/include/class_Traceable.hrl").
% The default, initial version for all classes (sole location where it is
% defined):
%
-define( default_initial_class_version, { 0, 0, 1 } ).
% Each concrete Upgradable class shall specify its own version like this:
-define( this_class_version, { 0, 0, 1 } ).
% Implementation notes:
%
% By default we rely on three-digit versions.
%
% Regarding code upgrade, this operation is by nature centralised (as it is VM /
% node level); this corresponds to performing an operation like:
% compile:file( ?MODULE ),
% code:purge( ?MODULE ),
% code:load_file( ?MODULE )
%
% ERTS can handle two versions of a module, and calling a function with
% mod:func(...) will always call the latest version of this module (if the
% function is exported).
%
% The new version of the module shall still be explicitly loaded into the
% system.
%
% When the new module version has been loaded, all instances of the
% corresponding class shall switch, here thanks to the upgrade/1 request.
%
% See also the upgrade_class/1 static method.
%
% As stated in the documentation (refer to
% https://www.erlang.org/doc/reference_manual/code_loading.html#code-replacement
% for more information), code replacement is done on a module level.
%
% The code of a module can exist in two variants in a system: current and
% old. When a module is loaded into the system for the first time, the code
% becomes 'current'. If then a new instance of the module is loaded, the code of
% the previous instance becomes 'old' and the new instance becomes 'current'.
%
% Both old and current code is valid, and can be evaluated concurrently. Fully
% qualified function calls always refer to current code. Old code can still be
% evaluated because of processes lingering in the old code.
%
% If a third instance of the module is loaded, the code server removes (purges)
% the old code and any processes lingering in it is terminated. Then the third
% instance becomes 'current' and the previously current code becomes 'old'.
%
% To change from old code to current code, a process must make a fully qualified
% function call.
%
% For code replacement of funs to work, use the syntax fun
% Module:FunctionName/Arity.
% See also
% https://learnyousomeerlang.com/designing-a-concurrent-application#hot-code-loving,
% and https://learnyousomeerlang.com/relups regarding the need to be able to
% freeze processes between the update of their module(s) and the processing of
% an 'upgrade' message. Such kind of "time suspension" is typically useful if
% the definition of records changed.
% Note that now whether an instance is Upgradable and what its current version
% is are determined rather differently: the former by determining whether its
% class inherits (directly or not) from Upgradable, the latter by calling a
% (presumably defined) static method to obtain that version.
% At least currently, there is no way to force that all Upgradable classes
% define such as static method.
% Type shorthands:
-type any_version() :: basic_utils:any_version().
-type base_status() :: basic_utils:base_status().
-type error_reason() :: basic_utils:error_reason().
-type base_outcome() :: basic_utils:base_outcome().
-type count() :: basic_utils:count().
-type ustring() :: text_utils:ustring().
-type define() :: code_utils:define().
-doc """
Constructs an upgradable instance.
The corresponding version is determined statically.
""".
-spec construct( wooper:state() ) -> wooper:state().
construct( State ) ->
% Traceable trait now optional:
%TraceState = class_Traceable:construct( State ),
%setAttribute( TraceState, wooper_upgradable_version,
% basic_utils:check_any_version( InitialVersion ) ).
State.
% No destructor.
% Methods section.
-doc "Returns the current version of this Upgradable.".
-spec getVersion( wooper:state() ) -> const_request_return( any_version() ).
getVersion( State ) ->
% Previously: CurrentVer = ?getAttr(wooper_upgradable_version),
CurrentVer = get_version( State ),
wooper:const_return_result( CurrentVer ).
-doc """
Upgrades this instance (thus to a more recent version) both in terms of code and
state, taking into account any specified extra data.
So performs an actual state upgrade between the two specified versions; in the
general case this may involve adding/removing attributes, changing their value
and/or type.
This implementation, meant to be overridden, does mostly nothing.
It is strongly recommended that, as done by default, an instance requested to
perform such update is in a frozen state (either explicitly created, or only
obtained by construction, for example if not being scheduled/triggered anymore
by some manager), lest it receives method calls between the overall class update
and the processing of the actual version change.
So this request should not be called directly, but whereas being already frozen
in the context of a prior freezeUntilVersionChange special call.
See also manage_version_change/4.
""".
-spec upgradeVersion( wooper:state(), any_version(), any_version(),
option( extra_data() ) ) -> request_return( base_outcome() ).
upgradeVersion( State, OriginalVersion, TargetVersion, MaybeExtraData ) ->
cond_utils:if_defined( wooper_debug_hot_update,
trace_bridge:debug_fmt( "Instance ~w upgrading from version ~ts to "
"~ts, using extra data '~p'.",
[ self(), text_utils:version_to_string( OriginalVersion ),
text_utils:version_to_string( TargetVersion ),
MaybeExtraData ] ),
basic_utils:ignore_unused( MaybeExtraData ) ),
cond_utils:if_defined( wooper_check_hot_update,
case get_version( State ) of
% Expected to be already at target:
TargetVersion ->
ok;
OtherVersion ->
throw( { invalid_version_on_upgrade, { read, OtherVersion },
{ original, OriginalVersion },
{ target, TargetVersion },
State#state_holder.actual_class, self() } )
end ),
% In this default implementation, the state remains const:
UpgradedState = State,
wooper:return_state_result( UpgradedState, ok ).
-doc """
Downgrades this instance (thus to a less recent version) both in terms of code
and state, taking into account any specified extra data.
So performs an actual state downgrade between the two specified versions; in the
general case this may involve adding/removing attributes, changing their value
and/or type.
This implementation, meant to be overridden, does mostly nothing.
It is strongly recommended that, as done by default, an instance requested to
perform such update is in a frozen state (either explicitly created, or only
obtained by construction, for example if not being scheduled/triggered anymore
by some manager), lest it receives method calls between the overall class update
and the processing of the actual version change.
So this request should not be called directly, but whereas being already frozen
in the context of a prior freezeUntilVersionChange special call.
See also manage_version_change/4.
""".
-spec downgradeVersion( wooper:state(), any_version(), any_version(),
option( extra_data() ) ) -> request_return( base_outcome() ).
downgradeVersion( State, OriginalVersion, TargetVersion, MaybeExtraData ) ->
cond_utils:if_defined( wooper_debug_hot_update,
trace_bridge:debug_fmt( "Downgrading from version ~ts to ~ts, "
"using extra data '~p'.",
[ text_utils:version_to_string( OriginalVersion ),
text_utils:version_to_string( TargetVersion ),
MaybeExtraData ] ),
basic_utils:ignore_unused( MaybeExtraData ) ),
cond_utils:if_defined( wooper_check_hot_update,
case get_version( State ) of
% Expected to be already at target:
TargetVersion ->
ok;
OtherVersion ->
throw( { invalid_version_on_upgrade, { read, OtherVersion },
{ original, OriginalVersion },
{ target, TargetVersion },
State#state_holder.actual_class, self() } )
end ),
% In this default implementation, the state remains const:
DowngradedState = State,
wooper:return_state_result( DowngradedState, ok ).
% Static section.
-doc """
Returns the version of that class (that corresponds to this module).
Each version of a class should define its own version of this static method.
""".
-spec get_version() -> static_return( any_version() ).
get_version() ->
% Each concrete Upgradable class is typically to return its own define:
wooper:return_static( ?this_class_version ).
-doc """
Freezes (synchronously) the specified instances, so that they are ready for an
update of their class to the specified version, with no extra data specified.
Returns a list (in no particular order) of freeze information, i.e. the
classname of each frozen instance, associated to its PID.
This implementation bypasses the WOOPER main loop (directly collecting messages,
instead of interpreting them as oneway calls).
""".
-spec freeze_instances( [ instance_pid() ], any_version() ) ->
static_return( [ freeze_info() ] ).
freeze_instances( InstancePids, TargetVersion ) ->
FreezeInfos = freeze_instances( InstancePids, TargetVersion,
_MaybeExtraData=undefined ),
wooper:return_static( FreezeInfos ).
-doc """
Freezes (synchronously) the specified instances, so that they are ready for an
update of their class to the specified version, with the specified extra data.
Returns a list (in no particular order) of the classname of each frozen
instance, associated to its PID.
This implementation bypasses the WOOPER main loop (directly collecting messages,
instead of interpreting them as oneway calls).
""".
-spec freeze_instances( [ instance_pid() ], any_version(),
option( extra_data() ) ) -> static_return( [ freeze_info() ] ).
freeze_instances( InstancePids, TargetVersion, MaybeExtraData ) ->
InstCount = length( InstancePids ),
cond_utils:if_defined( wooper_debug_hot_update,
trace_bridge:debug_fmt( "Freezing ~B instances: ~ts.",
[ InstCount, text_utils:pids_to_short_string( InstancePids ) ] ) ),
% The special message understood by the main loop of all Upgradables:
FreezeMsg = { freezeUntilVersionChange, TargetVersion, MaybeExtraData,
_CallerPid=self() },
[ IPid ! FreezeMsg || IPid <- InstancePids ],
FreezeInfos = collect_freeze_acks( InstCount ),
cond_utils:if_defined( wooper_debug_hot_update,
trace_bridge:debug_fmt( "~B instances frozen:~n ~p",
[ length( FreezeInfos ), FreezeInfos ] ) ),
% Now that in a stable situation, the class update can take place:
wooper:return_static( FreezeInfos ).
-doc """
Updates (globally) the specified class, by recompiling it, purging its current
implementation, and reloading it.
So operates at the class level, with no direct interaction with instances, not
selecting any particular version (the one of the updated class will just
apply). Typically all instances of that class have been already frozen (see the
freeze_instances/{2,3} static methods and the freezeUntilVersionChange special
call), waiting for the new code to be available and adapt to it.
ForceRecompilation tells whether the class module shall be forcibly recompiled;
useful for example if wanting to apply specific compilation options.
KillAnyLingeringProcess tells whether any process (probably an instance)
lingering on the old code shall be killed, or if the update shall just be
considered as having failed.
The first soft-purge will succeed even if an instance (e.g. agent C in
class_Upgradable_test) was not updated, yet the next module reloading will fail,
as the class will *not* be updated. If ignoring that failure, the old code will
attempt to operate on newer instance states, which of course should not be done.
""".
-spec update_class( classname(), boolean(), [ define() ], boolean() ) ->
static_return( base_status() ).
update_class( Classname, ForceRecompilation, Defines,
KillAnyLingeringProcess ) ->
% Refer to implementation notes.
% First, we recompile that class:
Res = case code_utils:recompile( Classname, ForceRecompilation, Defines ) of
ok ->
trace_utils:debug_fmt( "Class '~ts' successfully recompiled.",
[ Classname ] ),
purge_and_reload_class( Classname, KillAnyLingeringProcess );
RecompErr={ error, Reason } ->
trace_utils:error_fmt( "Class '~ts' could not be recompiled; "
"reason: ~ts.", [ Classname, Reason ] ),
RecompErr
end,
wooper:return_static( Res ).
-doc """
Requests the specified instance to update themselves (asynchronously) against
their new current (expected to have been updated) class.
These instances are expected to have been frozen beforehand (see
request_instances_to_update/1).
""".
-spec request_instances_to_update( [ instance_pid() ] ) -> static_return(
{ [ update_success_report() ], [ update_failure_report() ] } ).
request_instances_to_update( Instances ) ->
% The frozen instances already know the PID of this caller:
[ I ! updateFromUpgradableClass || I <- Instances ],
% Better than just counting, to be able to determine any lacking outcome:
WaitedSet = set_utils:from_list( Instances ),
AccPair = wait_update_outcomes( WaitedSet, _SuccessAcc=[], _FailureAcc=[] ),
wooper:return_static( AccPair ).
% Section for helper functions (not methods).
-doc """
Takes in charge the instance-side part of the version update protocol: freezes
until the class has been updated, and then applies the corresponding
instance-specific state changes.
Sends back to the caller first a freeze_notification() message, then, once
requested to update and having attempted to do so, an update_outcome() message.
""".
-spec manage_version_change( any_version(), extra_data(), pid(),
wooper:state() ) -> 'deleted'. % no_return().
manage_version_change( TargetVersion, MaybeExtraData, CallerPid, State ) ->
ActualClassMod = State#state_holder.actual_class,
% No interleaving possible; yet still safe to fetch by design, as the class
% has not been reloaded yet:
%
OriginalVersion = case get_maybe_version( State ) of
undefined ->
% Faulty upgradable or wrong instance:
throw( { no_get_version_defined, ActualClassMod, self() } );
V ->
V
end,
% Synchronisation needed, typically so that the caller can execute
% class_Upgradable:update_class/2 on its own only once all instances are
% frozen (i.e. not too early, whereas some of them may still be busy
% executing their current methods):
%
% (sending of a freeze_notification() possibly-oneway message, possibly
% interpreted as a oneway call, and preferably sending a pair instead of a
% list)
%
CallerPid ! { onInstanceFrozen, _FreezeInfo={ ActualClassMod, self() } },
% Trying to anticipate work as much as possible:
MaybeTargetRequest = case basic_utils:compare_versions( OriginalVersion,
TargetVersion ) of
second_bigger ->
cond_utils:if_defined( wooper_debug_hot_update,
trace_bridge:debug_fmt( "Instance ~w will upgrade from "
"version ~ts to ~ts.", [ self(),
text_utils:version_to_string( OriginalVersion ),
text_utils:version_to_string( TargetVersion ) ] ) ),
upgradeVersion;
equal ->
% Believed to be abnormal:
trace_bridge:warning_fmt( "No update will be done, as the original "
"and target versions are the same (~ts).",
[ text_utils:version_to_string( OriginalVersion ) ] ),
undefined;
first_bigger ->
cond_utils:if_defined( wooper_debug_hot_update,
trace_bridge:debug_fmt(
"Instance ~w will downgrade from version ~ts to ~ts.",
[ self(), text_utils:version_to_string( OriginalVersion ),
text_utils:version_to_string( TargetVersion ) ] ) ),
downgradeVersion
end,
Args = [ OriginalVersion, TargetVersion, MaybeExtraData ],
% Should the update succeed:
SuccessOutcome =
{ onUpdateSuccess, [ ActualClassMod, self(), TargetVersion ] },
% Fully prepared, just waiting then, frozen until the next selective receive
% is triggered:
%
% (any method calls pending in the mailbox to be processed just afterwards)
%
receive
% Sent only once the new class module has been loaded:
updateFromUpgradableClass ->
cond_utils:if_defined( wooper_debug_hot_update,
trace_bridge:debug_fmt( "Instance ~w requested now to update.",
[ self() ] ) ),
{ UpdateOutcome, FinalState } = case MaybeTargetRequest of
% Nothing done, still considering it a success:
undefined ->
{ SuccessOutcome, State };
TargetMethod ->
% Thanks to the virtual table (implying a module-qualified
% call), this will be resolved with the latest version of
% the modules involved:
%
{ UpdatedState, UpdateRes } =
executeRequest( State, TargetMethod, Args ),
%trace_utils:debug_fmt( "Updated state:~n ~p",
% [ UpdatedState ] ),
Outcome = case UpdateRes of
ok ->
SuccessOutcome;
{ error, ErrorReason } ->
% So we consider that we remained in the original
% version, retaining nevertheless the returned
% state (which is probably the original one):
%
{ onUpdateFailure, [ ErrorReason, ActualClassMod,
self(), OriginalVersion ] }
end,
{ Outcome, UpdatedState }
end,
CallerPid ! UpdateOutcome,
% The special-cased module-qualified call, to branch to the
% new implementation for this loop as well:
%
% (the update could even change the class name!)
%
FinalClassMod = FinalState#state_holder.actual_class,
cond_utils:if_defined( wooper_debug_hot_update,
%trace_bridge:debug_fmt( "Update done (outcome: ~p), "
% "state:~n ~p.", [ UpdateOutcome, FinalState ] ) ),
trace_bridge:debug_fmt( "Update done (outcome: ~p).",
[ UpdateOutcome ] ) ),
FinalClassMod:wooper_main_loop( FinalState )
end.
-doc "Collects the specified number of freeze information messages.".
-spec collect_freeze_acks( count() ) -> [ freeze_info() ].
collect_freeze_acks( InstCount ) ->
collect_freeze_acks( InstCount, _Acc=[] ).
% (helper)
collect_freeze_acks( _InstCount=0, Acc ) ->
% No order matters:
Acc;
collect_freeze_acks( InstCount, Acc ) ->
receive
% Could be stored in a table(classname(),[instance_pid()]) if useful:
{ onInstanceFrozen, FreezeInfoPair } ->
collect_freeze_acks( InstCount-1, [ FreezeInfoPair | Acc ] )
end.
-doc """
Returns the current version of this upgradable version.
(exported helper, defined for convenience)
""".
-spec get_version( wooper:state() ) -> any_version().
get_version( State ) ->
basic_utils:check_not_undefined( get_maybe_version( State ) ).
-doc "Returns a textual description of this instance.".
-spec to_string( wooper:state() ) -> ustring().
to_string( State ) ->
text_utils:format( "upgradable instance ~ts",
[ to_maybe_string( State ) ] ).
% The following helper functions can be used in the context of any class,
% whether or not it implements this Upgradable interface.
-doc """
Tells whether the corresponding instance implements the Upgradable interface.
(exported helper)
""".
-spec is_upgradable( wooper:state() ) -> boolean().
is_upgradable( State ) ->
% Previously instance-level:
%hasAttribute( State, wooper_upgradable_version ).
% Now statically determined:
lists:member( ?MODULE, wooper:get_all_superclasses( State ) ).
-doc """
Returns any version available for the corresponding instance.
This function is designed to apply to any WOOPER instance, whether it is a
Upgradable one or not.
(exported helper)
""".
-spec get_maybe_version( wooper:state() ) -> option( any_version() ).
get_maybe_version( State ) ->
ActualClassMod = State#state_holder.actual_class,
case meta_utils:is_function_exported( ActualClassMod, get_version,
_Arity=0 ) of
true ->
ActualClassMod:get_version();
false ->
undefined
end.
-doc """
Returns a textual element of description of the corresponding instance, should
it implement the Upgradable interface.
(exported helper)
""".
-spec to_maybe_string( wooper:state() ) -> option( ustring() ).
to_maybe_string( State ) ->
case get_maybe_version( State ) of
undefined ->
undefined;
AnyVer ->
text_utils:format( "whose version is ~ts",
[ text_utils:version_to_string( AnyVer ) ] )
end.
% Section for purely internal helpers.
-doc """
Purges and reloads the specified class, expected to be already recompiled.
(helper)
""".
-spec purge_and_reload_class( classname(), boolean() ) -> base_status().
purge_and_reload_class( Classname, KillAnyLingeringProcess ) ->
% Removes code marked as old - but only if no process lingers in it:
case code:soft_purge( Classname ) of
% No process to clear:
true ->
trace_utils:debug_fmt( "(soft purge of '~ts' succeeded)",
[ Classname ] ),
reload_class( Classname );
% Process(es) in the way:
false ->
case KillAnyLingeringProcess of
true ->
trace_utils:warning_fmt( "Classname '~ts' cannot be "
"soft-purged, as there is at least one process "
"lingering in the old code; such processes to be "
"killed now through a hard purge. "
"If this test VM gets killed in the process, ensure "
"that the module corresponding to this class was "
"not compiled with LCO disabled "
"(refer to the MYRIAD_LCO_OPT for that).",
[ Classname ] ),
case code:purge( Classname ) of
% Understood as being successful:
true ->
trace_utils:debug( "(purge succeeded)" ),
reload_class( Classname );
false ->
{ error, { purged_failed, Classname } }
end;
false ->
{ error, { lingering_processes, Classname } }
end
end.
-doc """
Reloads the specified class, expected to be already recompiled and purged.
(helper)
""".
-spec reload_class( classname() ) -> base_status().
reload_class( Classname ) ->
case code:load_file( Classname ) of
{ module, Classname } ->
trace_utils:debug_fmt( "Class '~ts' successfully reloaded.",
[ Classname ] ),
ok;
LoadErr={ error, Reason } ->
trace_utils:error_fmt( "Class '~ts' could not be reloaded; "
"reason: ~ts.", [ Classname, Reason ] ),
LoadErr
end.
% Waits the update outcomes to be received, for all instances in the specified
% set.
%
wait_update_outcomes( WaitedSet, SuccessAcc, FailureAcc ) ->
% Cannot be tested in a guard:
case set_utils:is_empty( WaitedSet ) of
true ->
{ SuccessAcc, FailureAcc };
false ->
receive
{ onUpdateSuccess, SuccessReport=[ _Classname, InstancePid,
_ResultingVersion ] } ->
NewWaitedSet =
set_utils:delete_existing( InstancePid, WaitedSet ),
wait_update_outcomes( NewWaitedSet,
[ SuccessReport | SuccessAcc ], FailureAcc );
{ onUpdateFailure, FailureReport=[ _ErrorReason, _Classname,
InstancePid, _ResultingVersion ] } ->
NewWaitedSet =
set_utils:delete_existing( InstancePid, WaitedSet ),
wait_update_outcomes( NewWaitedSet, SuccessAcc,
[ FailureReport | FailureAcc ] )
end
end.