% Copyright (C) 2012-2019 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 and % . % % Author: Olivier Boudeville [olivier (dot) boudeville (at) esperide (dot) com] % Module containing facilities for the serialisation and deserialisation of % WOOPER instances. % % Note: offseting as much as possible code from the counterpart class header in % this module allows for smaller, cache-friendly BEAMs, and short compilation % times. % -module(wooper_serialisation). % First, all kinds of exports: % For getAttr/1, and for setAttributes/2 and all being unused: -include("wooper_state_exports.hrl"). % For synchronous_time_out: -include("wooper_defines_exports.hrl"). % For attribute_name/0: -include("wooper_types_exports.hrl"). % Otherwise executeRequest/3 and all reported as unused: -include("wooper_execute_exports.hrl"). % To silence wooper_execute_method_as/4 being unused: -include("wooper_execute_internal_exports.hrl"). % To silence wooper_execute_method_as/4 and all being unused: -include("wooper_serialisation_exports.hrl"). % For myriad_spawn*: -include_lib("myriad/include/spawn_utils.hrl"). % Instance loading: -export([ load/1, load/3, load_link/1, load_link/3, synchronous_load/1, synchronous_load/3, synchronous_load_link/1, synchronous_load_link/3, remote_synchronisable_load_link/2, remote_synchronisable_load_link/4, remote_synchronous_timed_load_link/2, remote_synchronous_timed_load_link/4 ]). % Instance deserialisation: -export([ deserialise/4 ]). % Serialisation helpers: -export([ handle_private_processes/2, mute_attributes/2, check_attributes_equal/3, replace_attribute/3, replace_attributes/3, merge_list_for/3, merge_lists_for/3, list_restoration_markers/0 ]). % Any function that is able to transform the state of an instance. % % Note: see basic_utils:traverse_term/4, which may be useful in that context. % -type entry_transformer() :: maybe( fun( ( attribute_entry(), basic_utils:user_data() ) -> { attribute_entry(), basic_utils:user_data() } ) ). % The serialisation form of an instance, as an Erlang term: -type term_serialisation() :: [ attribute_entry() ]. % The serialisation form of an instance, as an ext_binary, i.e. a binary data % object, structured according to the Erlang external term format: % % However erlang:ext_binary/0 is not exported: %-type bin_serialisation() :: erlang:ext_binary(). % -type bin_serialisation() :: binary(). -export_type([ entry_transformer/0, term_serialisation/0, bin_serialisation/0 ]). -type restoration_marker() :: ?process_restoration_marker | ?file_restoration_marker | ?term_restoration_marker. % Exported as wooper_serialisation_functions.hrl has to be included, yet this % leads to have the function spotted as unused: % -export([ serialise/3 ]). % Now, function definitions: % For executeRequest/2: -include("wooper_execute_functions.hrl"). % For getAttribute/2, setAttribute/3, etc.: -include("wooper_state_functions.hrl"). % For wooper_deserialise/4: -include("wooper_serialisation_functions.hrl"). % For wooper_execute_method/3: -include("wooper_execute_internal_functions.hrl"). % In this section, we define for the current class load counterparts to new % operators, i.e. the various ways of creating an instance of it not through a % normal construction process, but from a deserialisation one (loading). % % For example, synchronous_new_link becomes synchronous_load_link. % Transformers are expected not to depend on the order of their calls, as % loadings can happen in parallel. % No updated user data is ever sent back, for the sake of API uniformity. % Spawns a new instance of this class, based on the specified serialisation % information. % % Returns the PID of the created instance for this loading. % % Creation is asynchronous: this function returns as soon as the creation is % triggered, without waiting for it to complete. % -spec load( bin_serialisation() ) -> instance_pid(). load( BinSerialisation ) -> load( BinSerialisation, _EntryTransformer=undefined, _UserData=undefined ). % Spawns a new instance of this class, based on the specified serialisation % information, entry transformer and user data. % % Returns the PID of the created instance for this loading. % % Creation is asynchronous: this function returns as soon as the creation is % triggered, without waiting for it to complete. % -spec load( bin_serialisation(), entry_transformer(), basic_utils:user_data() ) -> instance_pid(). load( BinSerialisation, EntryTransformer, UserData ) -> ?myriad_spawn( fun() -> deserialise( BinSerialisation, EntryTransformer, UserData, _ListenerPid=undefined ) end ). % Spawns a new instance of this class, based on the specified serialisation % information, and links it to the current process. % % Returns the PID of the created instance for this loading. % % Creation is asynchronous: this function returns as soon as the creation is % triggered, without waiting for it to complete. % -spec load_link( bin_serialisation() ) -> instance_pid(). load_link( BinSerialisation ) -> load_link( BinSerialisation, _EntryTransformer=undefined, _UserData=undefined ). % Spawns a new instance of this class, based on the specified serialisation % information, entry transformer and user data, and links it to the current % process. % % Returns the PID of the created instance for this loading. % % Creation is asynchronous: this function returns as soon as the creation is % triggered, without waiting for it to complete. % -spec load_link( bin_serialisation(), entry_transformer(), basic_utils:user_data() ) -> instance_pid(). load_link( BinSerialisation, EntryTransformer, UserData ) -> ?myriad_spawn_link( fun() -> deserialise( BinSerialisation, EntryTransformer, UserData, _ListenerPid=undefined ) end ). % Spawns a new instance of this class, based on the specified serialisation % information. % % Returns the PID of the created instance for this loading. % % Creation is synchronous: the call will return only when the created process % reports that it is up and running. % -spec synchronous_load( bin_serialisation() ) -> instance_pid(). synchronous_load( BinSerialisation ) -> synchronous_load( BinSerialisation, _EntryTransformer=undefined, _UserData=undefined ). % Spawns a new instance of this class, based on the specified serialisation % information, entry transformer and user data. % % Returns the PID of the created instance for this loading. % % Creation is synchronous: the call will return only when the created process % reports that it is up and running. % -spec synchronous_load( bin_serialisation(), entry_transformer(), basic_utils:user_data() ) -> instance_pid(). synchronous_load( BinSerialisation, EntryTransformer, UserData ) -> CreatorPid = self(), SpawnedPid = ?myriad_spawn( fun() -> deserialise( BinSerialisation, EntryTransformer, UserData, _ListenerPid=CreatorPid ) end ), % Blocks until the spawned process answers: receive { spawn_successful, SpawnedPid } -> SpawnedPid end. % Spawns a new instance of this class, based on the specified serialisation % information, and links it to the current process. % % Returns the PID of the created instance for this loading. % % Creation is synchronous: the call will return only when the created process % reports that it is up and running. % -spec synchronous_load_link( bin_serialisation() ) -> instance_pid(). synchronous_load_link( BinSerialisation ) -> synchronous_load_link( BinSerialisation, _EntryTransformer=undefined, _UserData=undefined ). % Spawns a new instance of this class, based on the specified serialisation % information, entry transformer and user data, and links it to the current % process. % % Returns the PID of the created instance for this loading. % % Creation is synchronous: the call will return only when the created process % reports that it is up and running. % -spec synchronous_load_link( bin_serialisation(), entry_transformer(), basic_utils:user_data() ) -> instance_pid(). synchronous_load_link( BinSerialisation, EntryTransformer, UserData ) -> CreatorPid = self(), SpawnedPid = ?myriad_spawn_link( fun() -> deserialise( BinSerialisation, EntryTransformer, UserData, _ListenerPid=CreatorPid ) end ), % Blocks until the spawned process answers: receive { spawn_successful, SpawnedPid } -> SpawnedPid end. % We did not feel the specific need to define: % % - synchronous_timed_load % - synchronous_timed_load_link % - remote_load % - remote_load_link % - remote_synchronous_load % - remote_synchronous_load_link % - remote_synchronous_timed_load % % (but they can be added if wanted) % Spawns on specified node a new instance of this class, based on the specified % serialisation information and links it to the current process. % % Returns the PID of the created instance. % % Creation is asynchronous (the PID is directly returned), however a { % spawn_successful, SpawnedPid } message will be received once (if ever) the % instance is up and running. This allows to perform the actual instance % creations in parallel, by waiting bulks of creations. % -spec remote_synchronisable_load_link( net_utils:node_name(), bin_serialisation() ) -> instance_pid(). remote_synchronisable_load_link( Node, BinSerialisation ) -> remote_synchronisable_load_link( Node, BinSerialisation, _EntryTransformer=undefined, _UserData=undefined ). % Spawns on specified node a new instance of this class, based on the specified % serialisation information, entry transformer and user data, and links it to % the current process. % % Returns the PID of the created instance. % % Creation is asynchronous (the PID is directly returned), however a { % spawn_successful, SpawnedPid } message will be received once (if ever) the % instance is up and running. This allows to perform the actual instance % creations in parallel, by waiting bulks of creations. % -spec remote_synchronisable_load_link( net_utils:node_name(), bin_serialisation(), entry_transformer(), basic_utils:user_data() ) -> instance_pid(). remote_synchronisable_load_link( Node, BinSerialisation, EntryTransformer, UserData ) -> CreatorPid = self(), ?myriad_spawn_link( Node, fun() -> deserialise( BinSerialisation, EntryTransformer, UserData, _ListenerPid=CreatorPid ) end ). % Spawns on specified node a new instance of this class, based on the specified % serialisation information, entry transformer and user data, and links it to % the current process. % % Returns the PID of the created instance for this loading, or the time_out % atom. % % Creation is synchronous: the call will return only when the created process % reports that it is up and running. % -spec remote_synchronous_timed_load_link( net_utils:node_name(), bin_serialisation() ) -> instance_pid(). remote_synchronous_timed_load_link( Node, BinSerialisation ) -> remote_synchronous_timed_load_link( Node, BinSerialisation, _EntryTransformer=undefined, _UserData=undefined ). % Spawns on specified node a new instance of this class, based on the specified % serialisation information, entry transformer and user data, and links it to % the current process. % % Returns the PID of the created instance for this loading, or the time_out % atom. % % Creation is synchronous: the call will return only when the created process % reports that it is up and running. % -spec remote_synchronous_timed_load_link( net_utils:node_name(), bin_serialisation(), entry_transformer(), basic_utils:user_data() ) -> instance_pid(). remote_synchronous_timed_load_link( Node, BinSerialisation, EntryTransformer, UserData ) -> CreatorPid = self(), SpawnedPid = ?myriad_spawn_link( Node, fun() -> deserialise( BinSerialisation, EntryTransformer, UserData, _ListenerPid=CreatorPid ) end ), % Blocks until the spawned process answers or a time-out occurs: receive { spawn_successful, SpawnedPid } -> SpawnedPid after ?synchronous_time_out -> io:format( "(remote_synchronous_timed_load_link: throwing time-out " "on node ~p for module ~p after ~p milliseconds)~n", [ Node, ?MODULE, ?synchronous_time_out ] ), throw( { remote_synchronous_linked_time_out, Node, ?MODULE } ) end. % Deserialises the specified instance from its serialised form (as a term, not % as a binary), to obtain its corresponding state, using specified entry % transformer and user data, then having the current executing process embody % this instance from then on. % % Does not return, as the WOOPER main loop will manage from then this just % deserialised instance. % % (helper, as the receiver process may not even be already a WOOPER instance) % % Note: the hosting process is not created here, as, for an increased % parallelism, we expect deserialisations to happen directly from the final % instance processes; we consider here that the process executing this helper is % the final host. % -spec deserialise( bin_serialisation(), entry_transformer(), basic_utils:user_data(), ListenerPid ) -> any() % no_return() when ListenerPid :: maybe( pid() ). deserialise( BinSerialisation, EntryTransformer, UserData, ListenerPid ) -> { Classname, SerialisedEntries } = binary_to_term( BinSerialisation ), % First we extract the WOOPER extra information: { RandomState, OtherEntries } = option_list:extract( wooper_random_state, SerialisedEntries ), HookedEntries = pre_deserialise_hook( { Classname, OtherEntries }, UserData ), VirtualTable = wooper:retrieve_virtual_table( Classname ), { TransformedEntries, FinalUserData } = case EntryTransformer of undefined -> { HookedEntries, UserData }; _ -> lists:foldl( EntryTransformer, _Acc0={ _ResultingEntries=[], UserData }, _List=HookedEntries ) end, % Sent as soon as available, rather than at the end: case ListenerPid of undefined -> ok; _-> ListenerPid ! { onDeserialisation, [ self(), FinalUserData ] } end, % Now we have the right attributes enumerated. % We need to bypass any constructor here. AttributeTable = ?wooper_table_type:add_entries( TransformedEntries, ?wooper_table_type:new() ), OptimisedAttributeTable = ?wooper_table_type:optimise( AttributeTable ), % Must be restored as well: case RandomState of undefined -> ok; _ -> random_utils:set_random_state( RandomState ) end, ForgedState = #state_holder{ virtual_table=VirtualTable, attribute_table=OptimisedAttributeTable, actual_class=Classname, request_sender=undefined }, % We could check here that no serialisation marker remains, with a specific % entry transformer and list_restoration_markers/0. FinalState = post_deserialise_hook( ForgedState ), % That's as simple as that! Classname:wooper_main_loop( FinalState ). % Handles private processes (through the name of the corresponding attributes), % i.e. processes that are internal to an instance that is to be serialised, so % that any next serialisation will see instead of their (former) PID a % serialisation marker. % % Returns an updated state. % % (helper, typically used in pre_serialise_hook/1, to avoid trying to serialise % internal processes) % -spec handle_private_processes( [ attribute_name() ], wooper:state() ) -> wooper:state(). handle_private_processes( PrivateAttributeNames, State ) -> lists:foldl( fun( PrivateAttrName, AccState ) -> NewValue = case getAttribute( AccState, PrivateAttrName ) of undefined -> undefined; Pid when is_pid( Pid ) -> % We just hide these PIDs on the serialised form: after % serialisation the live state will still reference them. % ?process_restoration_marker end, setAttribute( AccState, PrivateAttrName, NewValue ) end, _Acc0=State, _List=PrivateAttributeNames ). % Mutes specified attributes (i.e. replaces any attribute value not equal to % 'undefined' by a term restoration marker), for example so that they can escape % the serialisation process. % % Typically used in pre_serialise_hook/2, to store only relevant, useful % information. % % (helper) % -spec mute_attributes( [ attribute_name() ], wooper:state() ) -> wooper:state(). mute_attributes( AttributeNameList, State ) -> lists:foldl( fun( AttrName, AccState ) -> case hasAttribute( AccState, AttrName ) of true -> case getAttribute( AccState, AttrName ) of undefined -> % Let it as is: AccState; _ -> setAttribute( AccState, AttrName, ?term_restoration_marker ) end; false -> throw( { unknown_attribute, AttrName, AccState } ) end end, _Acc0=State, _List=AttributeNameList ). % Checks that the specified attributes have the same value in the specified % state and in the specified entries, otherwise throws an exception. % % (helper) % -spec check_attributes_equal( [ attribute_name() ], [ attribute_entry() ], wooper:state() ) -> void(). check_attributes_equal( _AttributeNames=[], _AttributeEntries, _State ) -> ok; check_attributes_equal( _AttributeNames=[ AttributeName | T ], AttributeEntries, State ) -> { AttributeValue, RemainingEntries } = option_list:extract( _K=AttributeName, AttributeEntries ), case ?getAttr(AttributeName) of AttributeValue -> check_attributes_equal( T, RemainingEntries, State ); OtherValue -> throw( { attribute_value_mismatch, AttributeName, { OtherValue, AttributeValue } } ) end. % Replaces the value held in the specified state by the one of the specified % attribute found in specified entry. % % (helper) % -spec replace_attribute( attribute_name(), [ attribute_entry() ], wooper:state() ) -> { [ attribute_entry() ], wooper:state() }. replace_attribute( AttributeName, AttributeEntries, State ) -> case hasAttribute( State, AttributeName ) of true -> { ToSetValue, RemainingEntries } = option_list:extract( _K=AttributeName, AttributeEntries ), NewState = setAttribute( State, AttributeName, ToSetValue ), { RemainingEntries, NewState }; false -> throw( { unknown_attribute, AttributeName, State } ) end. % Replaces the values held in the specified state by the ones of the specified % attributes found in specified entries. % % (helper) % -spec replace_attributes( [ attribute_name() ], [ attribute_entry() ], wooper:state() ) -> { [ attribute_entry() ], wooper:state() }. replace_attributes( AttributeNames, AttributeEntries, State ) -> lists:foldl( fun( AttrName, { AccEntries, AccState } ) -> replace_attribute( AttrName, AccEntries, AccState ) end, _Acc0={ AttributeEntries, State }, _List=AttributeNames ). % Extracts the value (supposedly, any type of list) of specified attribute from % specified entries, and append that list to the corresponding one found in the % specified state, stored under the same attribute name. % % Returns the remaining entries, and an updated state. % -spec merge_list_for( attribute_name(), [ attribute_entry() ], wooper:state() ) -> { [ attribute_entry() ], wooper:state() }. merge_list_for( AttributeName, AttributeEntries, State ) -> { ToMergeValue, RemainingEntries } = option_list:extract( _K=AttributeName, AttributeEntries ), InitialValue = ?getAttr(AttributeName), MergedValue = case ToMergeValue of PlainList when is_list( PlainList ) -> InitialValue ++ PlainList; % We suppose it is a set (that cannot match a list) Set -> set_utils:union( InitialValue, Set ) end, MergedState = setAttribute( State, AttributeName, MergedValue ), { RemainingEntries, MergedState }. % Extracts the value (supposedly, any type of list) of each of the specified % attributes from specified entries, and append that list to the corresponding % one found in the specified state, stored under the same attribute name. % % Returns the remaining entries, and an updated state. % -spec merge_lists_for( [ attribute_name() ], [ attribute_entry() ], wooper:state() ) -> { [ attribute_entry() ], wooper:state() }. merge_lists_for( AttributeNames, AttributeEntries, State ) -> lists:foldl( fun( AttrName, { AccEntries, AccState } ) -> merge_list_for( AttrName, AccEntries, AccState ) end, _Acc0={ AttributeEntries, State }, _List=AttributeNames ). % Returns a list of the known restoration markers. % % (helper) % -spec list_restoration_markers() -> [ restoration_marker() ]. list_restoration_markers() -> [ ?process_restoration_marker, ?file_restoration_marker, ?term_restoration_marker ].