% 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 some facilities for WOOPER users, better placed in a % standalone module than duplicated in each class. % -module(wooper). % First, all the various exports: % Very generic: -export([ get_classname/1, get_attribute_pairs/1, state_to_string/1, get_class_filename/1 ]). % Settings helpers: -export([ get_synchronous_time_out/1 ]). % Communication helpers for active instances: -export([ execute_request/3, execute_request/4, send_requests/3, send_requests_and_wait_acks/4, send_requests_and_wait_acks/5, wait_for_request_answers/2, wait_for_request_answers/3, wait_for_request_acknowledgements/2, obtain_results_for_requests/3, send_request_series/2, obtain_results_for_request_series/2, send_and_listen/3, receive_result/0 ]). % Creation helpers: -export([ create_hosting_process/2, construct_and_run/2, construct_and_run_synchronous/3, construct_passive/2 ]). % Destruction helpers: -export([ delete_any_instance_referenced_in/2, delete_synchronously_any_instance_referenced_in/2, safe_delete_synchronously_any_instance_referenced_in/2, delete_synchronously_instance/1, delete_synchronously_instances/1, safe_delete_synchronously_instances/1, delete_passive/1 ]). % Method execution for passive instances: -export([ execute_request/2, % already exported: execute_request/3, execute_oneway/2, execute_oneway/3 ]). % Infrequently-called functions for state management: -export([ get_all_attributes/1 ]). % Extra features: -export([ declare_beam_dirs_for_wooper/0, retrieve_virtual_table/1 ]). % Traps to detect any method terminator that would be left untransformed. % % Note: neither exported or even defined anymore, as now WOOPER checks at % compile time whether a given wooper:SomeFun(...) call is a terminator. % -export([ return_state_result/2, return_state/1, return_static/1, const_return_result/1, const_return/0 ]). % For log and error reporting: -export([ log_info/1, log_info/2, log_warning/1, log_warning/2, log_error/1, log_error/2, log_error/3, on_failed_request/7, on_failed_oneway/6 ]). -ifdef(wooper_debug). % State-related helpers (only available in debug mode): -export([ virtual_table_to_string/1, instance_to_string/1, display_state/1, display_virtual_table/1, display_instance/1 ]). -else. % wooper_debug % Exported as otherwise reported as unused: -export([ check_classname_and_arity/2 ]). -endif. % wooper_debug % Basics: -export([ default_exit_handler/3, default_down_handler/5, default_node_up_handler/3, default_node_down_handler/3 ]). % To allow for finer checking: -export([ get_exported_functions_set/0 ]). % The record defining the state of a passive instance: -define( passive_record, state_holder ). % For the name of the registered process that keeps the per-class method % hashtables: % -include("wooper_class_manager.hrl"). % For synchronous_time_out, state_holder and al: -include("wooper_defines_exports.hrl"). % Actual definitions the shorthands in wooper_types_exports.hrl refer to: % An atom prefixed with 'class_': -type classname() :: atom(). % A method name (ex: 'setColor'): -type method_name() :: meta_utils:function_name(). % Name of a request method: -type request_name() :: method_name(). % Name of a oneway method: -type oneway_name() :: method_name(). % Name of a static method: -type static_name() :: method_name(). % Arity of a method: -type method_arity() :: meta_utils:function_arity(). -type request_arity() :: method_arity(). -type oneway_arity() :: method_arity(). -type static_arity() :: method_arity(). -type method_id() :: { method_name(), method_arity() }. -type request_id() :: { request_name(), request_arity() }. -type oneway_id() :: { oneway_name(), oneway_arity() }. -type static_id() :: { static_name(), static_arity() }. % Access qualifier, applying either to a method or to an attribute: -type access_qualifier() :: 'public' | 'protected' | 'private'. % A method argument can be any type: -type method_argument() :: any(). % Standalone (non-list) arguments may also be specified: -type method_arguments() :: method_argument() | [ method_argument() ]. % Qualifiers applying to methods: -type method_qualifier() :: access_qualifier() % This method cannot be overridden: | 'final' % This method does not change the state of the % instance it is applied on: % % (only meaningful for requests and oneways) % | 'const'. % The qualifiers applying to a method: -type method_qualifiers() :: [ method_qualifier() ]. % Special case of construction parameters: -type construction_parameter() :: method_argument(). -type construction_parameters() :: [ construction_parameter() ]. -type requests_outcome() :: 'success' | { 'failure', [ pid() ] }. % To be specified more closely maybe: -type method_internal_result() :: any(). % To describe all kinds of results: -type request_result( T ) :: T. -type request_result() :: request_result( any() ). -type static_result( T ) :: T. -type static_result() :: static_result( any() ). % For method specs: -type request_return( T ) :: { state(), request_result( T ) }. -type const_request_return( T ) :: request_result( T ). -type oneway_return() :: state(). -type const_oneway_return() :: void(). -type static_return( T ) :: static_result( T ). % Constness irrelevant for static methods. -type attribute_name() :: atom(). -type attribute_value() :: any(). -type attribute_entry() :: { attribute_name(), attribute_value() }. % The type-as-a-term of an attribute: -type attribute_type() :: type_utils:type(). % Qualifiers applying to attributes: -type attribute_qualifier() :: % The initial value of that attribute cannot be modified: 'const'. % PID of a WOOPER instance. -type instance_pid() :: pid(). % Passive instance: -type passive_instance() :: state(). % Atom used to denote an acknowlegment (i.e. a conventional symbol to ensure % that a request was synchronously executed): % -type ack_atom() :: atom(). % Otherwise wooper_execute_method_as/4 is unused: -include("wooper_execute_internal_exports.hrl"). %-opaque state() :: #state_holder{}. -type state() :: #state_holder{}. % Allows to record the functions exported by a module (typically the 'wooper' % one): % -type function_export_set() :: set_utils:set( meta_utils:function_id() ). % We prefer having it prefixed by wooper: -export_type([ classname/0, method_name/0, request_name/0, oneway_name/0, static_name/0, method_arity/0, method_id/0, request_id/0, oneway_id/0, static_id/0, access_qualifier/0, method_argument/0, method_arguments/0, method_qualifier/0, method_qualifiers/0, construction_parameter/0, construction_parameters/0, requests_outcome/0, method_internal_result/0, request_result/1, request_result/0, static_result/1, static_result/0, request_return/1, const_request_return/1, oneway_return/0, const_oneway_return/0, static_return/1, attribute_name/0, attribute_value/0, attribute_entry/0, attribute_type/0, attribute_qualifier/0, instance_pid/0, passive_instance/0, state/0, function_export_set/0 ]). % Note: the {attribute, request, oneway, static, class}_info/0 types are % exported from wooper_info. % For getAttr and al: -include("wooper_state_exports.hrl"). % Otherwise executeRequest/3 and all reported as unused: -include("wooper_execute_exports.hrl"). %-include("wooper_execute_internal_exports.hrl"). % Now, function definitions: % For getAttribute/2 and al: -include("wooper_state_functions.hrl"). % For executeRequest/2: -include("wooper_execute_functions.hrl"). % For wooper_execute_method/3: -include("wooper_execute_internal_functions.hrl"). % For get_superclasses/0: %-include("wooper_classes_functions.hrl"). % For myriad_spawn*: -include("spawn_utils.hrl"). % Section for communication helpers. % % Generally no wooper result expected to be already in the message queue or to % be received during these operations. % A WOOPER request execution (hence a synchronous call) not yielding a result in % specified duration (in milliseconds) will lead to a notification to be % displayed on the console (yet the result will still be waited for % indefinitively) % -ifdef(wooper_debug). -define( notify_long_wait_after, 2500 ). -else. % wooper_debug -define( notify_long_wait_after, 60000 ). -endif. % wooper_debug % First, just a simple shorthand: one caller, one request, one result, waited % indefinitively. % Sends specified request to specified (active or passive) instance, if active % waits indefinitively for its returned value (supposing none is already waiting % among the received messages), and returns it. % % (public helper, as a convenience wrapper or for passive instances) % -spec execute_request( instance_pid(), request_name() ) -> request_result(); ( passive_instance(), request_name() ) -> { passive_instance(), method_internal_result() }. execute_request( TargetInstancePID, RequestName ) when is_pid( TargetInstancePID ) andalso is_atom( RequestName ) -> RequestArgs = [], TargetInstancePID ! { RequestName, RequestArgs, self() }, execute_request_waiter( TargetInstancePID, RequestName, RequestArgs ); execute_request( PassiveInstance, RequestName ) when is_record( PassiveInstance, ?passive_record ) andalso is_atom( RequestName ) -> { NewPassiveInstance, { wooper_result, R } } = wooper_execute_method( RequestName, _RequestArgs=[], PassiveInstance ), { NewPassiveInstance, R }. % Sends specified request to specified (active or passive) instance, if active % waits indefinitively for its returned value (supposing none is already waiting % among the received messages), and returns it. % % (public helper, as a convenience wrapper or for passive instances) % -spec execute_request( instance_pid(), request_name(), method_arguments() ) -> request_result(); ( passive_instance(), request_name(), method_arguments() ) -> { passive_instance(), method_internal_result() }. execute_request( TargetInstancePID, RequestName, RequestArgs ) when is_pid( TargetInstancePID ) andalso is_atom( RequestName ) -> TargetInstancePID ! { RequestName, RequestArgs, self() }, execute_request_waiter( TargetInstancePID, RequestName, RequestArgs ); execute_request( PassiveInstance, RequestName, RequestArgs ) when is_record( PassiveInstance, ?passive_record ) andalso is_atom( RequestName ) -> { NewPassiveInstance, { wooper_result, R } } = wooper_execute_method( RequestName, RequestArgs, PassiveInstance ), { NewPassiveInstance, R }. % (helper) execute_request_waiter( TargetInstancePID, RequestName, RequestArgs ) -> receive { wooper_result, Res } -> Res after ?notify_long_wait_after -> trace_utils:warning_fmt( "Still awaiting an answer " "from WOOPER instance ~p, after having called " "request '~s' on it with following " "parameters:~n~p", [ TargetInstancePID, RequestName, RequestArgs ] ), execute_request_waiter( TargetInstancePID, RequestName, RequestArgs ) end. % Sends specified request to specified instance and waits indefinitively for its % specified, expected returned value (supposing none is already waiting among % the received messages) that is usually an atom. % % (helper) % -spec execute_request( request_name(), method_arguments(), instance_pid(), any() ) -> void(). execute_request( RequestName, RequestArgs, TargetInstancePID, ExpectedResult ) -> TargetInstancePID ! { RequestName, RequestArgs, self() }, execute_request_waiter( ExpectedResult, TargetInstancePID, RequestName, RequestArgs ). % (helper) execute_request_waiter( ExpectedResult, TargetInstancePID, RequestName, RequestArgs ) -> receive { wooper_result, ExpectedResult } -> ok after ?notify_long_wait_after -> trace_utils:warning_fmt( "Still awaiting the expected answer '~p' " "from WOOPER instance ~p, after having called " "request '~s' on it with following " "parameters:~n~p", [ ExpectedResult, TargetInstancePID, RequestName, RequestArgs ] ), execute_request_waiter( ExpectedResult, TargetInstancePID, RequestName, RequestArgs ) end. % Second: one caller, one request, multiple callees, with a time-out or not. % Sends specified request (based on its names and arguments) to each of the % specified target instances. % % (helper) % -spec send_requests( request_name(), method_arguments(), [ instance_pid() ] ) -> void(). send_requests( RequestName, RequestArgs, TargetInstancePIDs ) -> Request = { RequestName, RequestArgs, self() }, [ InstancePid ! Request || InstancePid <- TargetInstancePIDs ]. % Sends specified request (based on its name and arguments) to each of the % specified target instances, and waits (indefinitively) for their % acknowledgement, which shall be their only result (i.e. these methods should % be requests only for synchronisation). % % No time-out: answers will be waited indefinitely. % -spec send_requests_and_wait_acks( request_name(), method_arguments(), [ instance_pid() ], ack_atom() ) -> void(). send_requests_and_wait_acks( RequestName, RequestArgs, TargetInstancePIDs, AckAtom ) -> send_requests( RequestName, RequestArgs, TargetInstancePIDs ), wait_indefinitively_for_request_answers( TargetInstancePIDs, AckAtom ). % Sends specified request (based on its names and arguments) to each of the % specified target instances, and waits for their acknowledgement; returns % whether it succeeded or if some instances triggered a time-out. % -spec send_requests_and_wait_acks( request_name(), method_arguments(), [ instance_pid() ], time_utils:time_out(), ack_atom() ) -> requests_outcome(). send_requests_and_wait_acks( RequestName, RequestArgs, TargetInstancePIDs, Timeout, AckAtom ) -> send_requests( RequestName, RequestArgs, TargetInstancePIDs ), wait_for_request_answers( TargetInstancePIDs, Timeout, AckAtom ). % Waits for an acknowledgement answer, based on specified atom and on the PID of % each of the specified requested instances, indefinitively (no time-out). % % Allows to trigger requests (supposing returning all the same, specified atom) % in parallel yet being able to wait synchronously for them, and know which, if % any, did not answer. % % (helper) % -spec wait_for_request_answers( [ instance_pid() ], ack_atom() ) -> requests_outcome(). wait_for_request_answers( RequestedPidList, AckAtom ) -> wait_indefinitively_for_request_answers( RequestedPidList, AckAtom ). % Waits for an acknowledgement answer, based on specified atom, from the % specified requested instances, unless the specified time-out is exceeded % (specified as integer milliseconds or the 'infinity' atom). % % Allows to trigger requests in parallel yet being able to wait synchronously % for them. % % (helper) % -spec wait_for_request_answers( [ instance_pid() ], time_utils:time_out(), ack_atom() ) -> requests_outcome(). wait_for_request_answers( RequestedPidList, _Timeout=infinity, AckAtom ) -> wait_indefinitively_for_request_answers( RequestedPidList, AckAtom ); wait_for_request_answers( RequestedPidList, Timeout, AckAtom ) -> InitialTimestamp = time_utils:get_timestamp(), wait_for_request_answers( RequestedPidList, InitialTimestamp, Timeout, AckAtom ). % Waits until end of time if necessary. % % (helper) % wait_indefinitively_for_request_answers( _RequestedPidList=[], _AckAtom ) -> success; wait_indefinitively_for_request_answers( RequestedPidList, AckAtom ) -> receive { wooper_result, { AckAtom, SenderPid } } -> NewPidList = list_utils:delete_existing( SenderPid, RequestedPidList ), wait_indefinitively_for_request_answers( NewPidList, AckAtom ) end. % Wait until specified time-out is reached. % % (helper) % wait_for_request_answers( RequestedPidList, InitialTimestamp, Timeout, AckAtom ) -> wait_for_request_answers( RequestedPidList, InitialTimestamp, Timeout, _DefaultPollDuration=1000, AckAtom ). wait_for_request_answers( _RequestedPidList=[], _InitialTimestamp, _Timeout, _PollDuration, _AckAtom ) -> success; wait_for_request_answers( RequestedPidList, InitialTimestamp, Timeout, PollDuration, AckAtom ) -> receive { wooper_result, { AckAtom, SenderPid } } -> NewPidList = list_utils:delete_existing( SenderPid, RequestedPidList ), wait_for_request_answers( NewPidList, InitialTimestamp, Timeout, PollDuration, AckAtom ) after PollDuration -> NewDuration = time_utils:get_duration_since( InitialTimestamp ), case NewDuration > Timeout of true -> { failure, RequestedPidList }; false -> % Still waiting then: wait_for_request_answers( RequestedPidList, InitialTimestamp, Timeout, PollDuration, AckAtom ) end end. % Waits (indefinitively) that the specified number of requests returned as % result the specified acknowledgement atom. % -spec wait_for_request_acknowledgements( basic_utils:count(), ack_atom() ) -> void(). wait_for_request_acknowledgements( _Count=0, _AckAtom ) -> %trace_utils:debug_fmt( % "[~w] No more waiting of the '~s' acknowledgement atom.", % [ self(), AckAtom ] ), ok; wait_for_request_acknowledgements( Count, AckAtom ) -> %trace_utils:debug_fmt( "[~w] Waiting for ~B '~s' acknowledgement atom(s).", % [ self(), Count, AckAtom ] ), receive { wooper_result, AckAtom } -> %trace_utils:debug_fmt( % "[~w] Received a '~s' acknowledgement atom.", % [ self(), AckAtom ] ), wait_for_request_acknowledgements( Count-1, AckAtom ) end. % Sends the specified request to all specified instances for execution, in % parallel, and returns the corresponding results, in indiscriminated order. % % Note: no specified order is enforced in the result list; hence this helper is % meant to be used when we can collect each result regardless of its specific % sender. % % No time-out enforced. % % (exported helper) % -spec obtain_results_for_requests( request_name(), method_arguments(), [ instance_pid() ] ) -> [ request_result() ]. obtain_results_for_requests( RequestName, RequestArgs, TargetInstancePIDs ) -> send_requests( RequestName, RequestArgs, TargetInstancePIDs ), % Of course we expect that no previously received WOOPER message is % remaining in the queue. collect_wooper_messages( _Count=length( TargetInstancePIDs ), _Acc=[] ). % Collects specified number of WOOPER messages, and returns a list of the % corresponding results. % % (helper) % collect_wooper_messages( _Count=0, Acc ) -> Acc; collect_wooper_messages( Count, Acc ) -> receive { wooper_result, Res } -> collect_wooper_messages( Count-1, [ Res | Acc ] ) end. % Second: one caller,multiple requests, one callee (expected to answer them in % order - which is the general case in WOOPER where there is no selective % receive). % % No other results expected to be received once the request series has been % triggered. No time-out enforced. % Sends specified series of requests (based on its names and arguments) to the % specified target instance. % % (helper) % -spec send_request_series( [ { request_name(), method_arguments() } ], instance_pid() ) -> void(). send_request_series( _Requests=[], _TargetInstancePID ) -> ok; send_request_series( _Requests=[ { RequestName, RequestArgs } | T ], TargetInstancePID ) -> ActualRequest = { RequestName, RequestArgs, self() }, TargetInstancePID ! ActualRequest, send_request_series( T, TargetInstancePID ). % Sends the specified request to all specified instances for execution, in % parallel, and returns the corresponding results, in indiscriminated order. % % Note: no specified order is enforced in the result list; hence this helper is % meant to be used when we can collect each result regardless of its specific % sender. % % (exported helper) % -spec obtain_results_for_request_series( [ { request_name(), method_arguments() } ], instance_pid() ) -> [ request_result() ]. obtain_results_for_request_series( Requests, TargetInstancePID ) -> send_request_series( Requests, TargetInstancePID ), % Requests sent in-order, so will answers be received: wait_request_series( _WaitCount=length( Requests ), _Acc=[] ). % (helper) wait_request_series( _WaitCount=0, Acc ) -> lists:reverse( Acc ); wait_request_series( WaitCount, Acc ) -> receive { wooper_result, R } -> wait_request_series( WaitCount-1, [ R | Acc ] ) end. % Section for creation helpers. % Creates (asynchronously) a blank process, waiting to embody a WOOPER instance % once it will have received its class and construction parameters, and link it % to the caller and to the specified process. % -spec create_hosting_process( net_utils:node_name(), pid() ) -> instance_pid(). create_hosting_process( Node, ToLinkWithPid ) -> WaitFun = fun() -> % Closure; not atomic: erlang:link( ToLinkWithPid ), receive { embody, [ Class, ConstructionParameters ] } -> %trace_utils:debug_fmt( % "Process ~w becoming asynchronously an instance " % "of class '~s', constructed from following " % "parameters:~n~p.", % [ self(), Class, ConstructionParameters ] ), % Never returns: construct_and_run( Class, ConstructionParameters ); % We might need to notify another process than the caller: { embody, [ Class, ConstructionParameters ], ToNotifyPid } -> %trace_utils:debug_fmt( % "Process ~w becoming synchronously an instance " % "of class '~s', constructed from following " % "parameters:~n~p.", % [ self(), Class, ConstructionParameters ] ), % Never returns: construct_and_run_synchronous( Class, ConstructionParameters, ToNotifyPid ) end end, ?myriad_spawn_link( Node, WaitFun ). % Checks, for the specified classname and construction parameters, that a % corresponding module exists and that it has the relevant arity. % -spec check_classname_and_arity( classname(), construction_parameters() ) -> void(). check_classname_and_arity( Classname, ConstructionParameters ) -> % Normally useless, as called by the module itself: case code_utils:is_beam_in_path( Classname ) of not_found -> throw( { beam_not_found_for, Classname } ); _ -> ok end, % Includes the state: ArgCount = length( ConstructionParameters ) + 1, case meta_utils:is_function_exported( _Module=Classname, _Function=construct, _Arity=ArgCount ) of true -> ok; false -> ExportedFunctions = meta_utils:list_exported_functions( Classname ), case _ConstructArities=[ Arity || { construct, Arity } <- ExportedFunctions ] of [] -> trace_utils:error_fmt( "Error, no 'construct' exported " "in '~s' (regardless of arity).", [ Classname ] ), throw( { no_exported_construct, Classname } ); [ FoundArity ] when FoundArity > ArgCount -> ExtraCount = FoundArity - ArgCount, trace_utils:error( "Error, no ~s:construct/~B found, " "whereas construct/~B is exported; ~B extra " "construction parameter(s) specified.", [ Classname, ArgCount, FoundArity, ExtraCount ] ), throw( { extra_construction_parameters_specified, Classname, ExtraCount } ); % Here ArgCount > FoundArity: [ FoundArity ] -> LackingCount = ArgCount - FoundArity, trace_utils:error( "Error, no ~s:construct/~B found, " "whereas construct/~B is exported; ~B lacking " "construction parameter(s) specified.", [ Classname, ArgCount, FoundArity, LackingCount ] ), throw( { lacking_construction_parameters_specified, Classname, LackingCount } ); ConstructArities -> trace_utils:error( "Error, no ~s:construct/~B found, " "whereas this function is exported for following " "arities: ~w.", [ Classname, ArgCount, ConstructArities ] ), throw( { invalid_construction_parameters_specified, Classname, ArgCount, ConstructArities } ) end end. % Constructs the initial state of an instance of specified class, using % specified construction parameters, and enters its main loop. % % (helper) % -spec construct_and_run( classname(), construction_parameters() ) -> no_return(). -ifdef(wooper_debug). construct_and_run( Classname, ConstructionParameters ) -> %trace_utils:debug_fmt( "wooper:construct_and_run for class ~p " % "and parameters ~p.", % [ Classname, ConstructionParameters ] ), %check_classname_and_arity( Classname, ConstructionParameters ), BlankState = get_blank_state( Classname ), try apply( Classname, construct, [ BlankState | ConstructionParameters ] ) of ConstructState when is_record( ConstructState, state_holder ) -> % Enforces a closer-to-ideal load factor of the hashtable if needed, % as by convention no attribute should be introduced outside of the % constructor: % % (now useless with more advanced tables) % %TunedTable = ?wooper_table_type:optimise( %AttrTable = ConstructState#state_holder.attribute_table, %ReadyState = ConstructState#state_holder{ % attribute_table=AttrTable }, % Otherwise, in wooper_destruct/1 and all, ?MODULE will be 'wooper' % instead of the right class: % Classname:wooper_main_loop( ConstructState ); Other -> log_error( "~nWOOPER error for PID ~w of class ~s: " "constructor did not return a state, but returned '~p' " "instead. Construction parameters were:~n~p.", [ self(), Classname, Other, ConstructionParameters ] ), Arity = length( ConstructionParameters ) + 1, throw( { invalid_constructor, Classname, { construct, Arity } } ) catch Reason:ErrorTerm:Stacktrace -> trigger_error( Reason, ErrorTerm, Classname, ConstructionParameters, Stacktrace ) end. -else. % wooper_debug construct_and_run( Classname, ConstructionParameters ) -> BlankState = get_blank_state( Classname ), ConstructState = try apply( Classname, construct, [ BlankState | ConstructionParameters ] ) catch Reason:ErrorTerm:Stacktrace -> trigger_error( Reason, ErrorTerm, Classname, ConstructionParameters, Stacktrace ) end, % Enforces a closer-to-ideal load factor of the hashtable if needed, as by % convention no attribute should be introduced outside of the constructor: % %TunedTable = ?wooper_table_type:optimise( % ConstructState#state_holder.attribute_table ), %ReadyState = ConstructState#state_holder{ attribute_table=TunedTable }, % Otherwise, in wooper_destruct/1 and all, ?MODULE will be 'wooper' instead % of the right class: % Classname:wooper_main_loop( ConstructState ). -endif. % wooper_debug % Constructs synchronously the initial state of an instance of specified class, % using specified construction parameters, and enters its main loop. % % (helper) % -spec construct_and_run_synchronous( classname(), construction_parameters(), pid() ) -> no_return(). -ifdef(wooper_debug). construct_and_run_synchronous( Classname, ConstructionParameters, SpawnerPid ) -> %check_classname_and_arity( Classname, ConstructionParameters ), BlankState = get_blank_state( Classname ), try apply( Classname, construct, [ BlankState | ConstructionParameters ] ) of ConstructState when is_record( ConstructState, state_holder ) -> % Notify early: SpawnerPid ! { spawn_successful, self() }, % Enforces a closer-to-ideal load factor of the hashtable if needed, % as by convention no attribute should be introduced outside of the % constructor: % % (now useless with more advanced tables) % % TunedTable = ?wooper_table_type:optimise( AttrTable = ConstructState#state_holder.attribute_table, ReadyState = ConstructState#state_holder{ attribute_table=AttrTable }, % Otherwise, in wooper_destruct/1 and all, ?MODULE will be 'wooper' % instead of the right class: % % (never returns) % Classname:wooper_main_loop( ReadyState ); Other -> log_error( "~nWOOPER error for PID ~w of class ~s: " "constructor did not return a state, but returned '~p' " "instead. Construction parameters were:~n~p.~n", [ self(), Classname, Other, ConstructionParameters ] ), Arity = length( ConstructionParameters ) + 1, throw( { invalid_constructor, Classname, { construct, Arity } } ) catch Reason:ErrorTerm:Stacktrace -> trigger_error( Reason, ErrorTerm, Classname, ConstructionParameters, Stacktrace ) end. -else. % not in wooper_debug: construct_and_run_synchronous( Classname, ConstructionParameters, SpawnerPid ) -> BlankState = get_blank_state( Classname ), % Faulty returns (non-state) not detected here: ConstructState = try apply( Classname, construct, [ BlankState | ConstructionParameters ] ) catch Reason:ErrorTerm:Stacktrace -> trigger_error( Reason, ErrorTerm, Classname, ConstructionParameters, Stacktrace ) end, % Notify early: SpawnerPid ! { spawn_successful, self() }, % Enforces a closer-to-ideal load factor of the hashtable if needed, as by % convention no attribute should be introduced outside of the constructor: % % (now useless with more advanced tables) %TunedTable = ?wooper_table_type:optimise( AttrTable = ConstructState#state_holder.attribute_table, ReadyState = ConstructState#state_holder{ attribute_table=AttrTable }, % Otherwise, in wooper_destruct/1 and all, ?MODULE will be 'wooper' instead % of the right class: % % (never returns) % Classname:wooper_main_loop( ReadyState ). -endif. % not wooper_debug. % Section for passive instances. % Constructs a passive instance: returns the initial state thereof. -spec construct_passive( classname(), construction_parameters() ) -> passive_instance(). construct_passive( Classname, ConstructionParameters ) -> %trace_utils:debug_fmt( "wooper:construct_passive for class ~s " % "and parameters ~p.", % [ Classname, ConstructionParameters ] ), cond_utils:if_defined( wooper_debug, check_classname_and_arity( Classname, ConstructionParameters ) ), BlankState = get_blank_state( Classname ), try apply( Classname, construct, [ BlankState | ConstructionParameters ] ) of ConstructState when is_record( ConstructState, state_holder ) -> ConstructState; Other -> log_error( "WOOPER error when creating a passive instance " "of class ~s: constructor did not return a state, " "but returned '~p' instead. " "Construction parameters were:~n~p", [ Classname, Other, ConstructionParameters ] ), Arity = length( ConstructionParameters ) + 1, throw( { invalid_constructor, Classname, { construct, Arity } } ) catch Reason:ErrorTerm:Stacktrace -> trigger_error( Reason, ErrorTerm, Classname, ConstructionParameters, Stacktrace ) end. % execute_request/3 defined together with its convenience helper counterpart % (same name, same arity, different purposes). % Executes specified oneway on specified passive instance. -spec execute_oneway( passive_instance(), oneway_name() ) -> passive_instance(). execute_oneway( PassiveInstance, OnewayName ) when is_record( PassiveInstance, ?passive_record ) andalso is_atom( OnewayName ) -> { NewPassiveInstance, { wooper_method_returns_void, R } } = wooper_execute_method( OnewayName, _OnewayArgs=[], PassiveInstance ), { NewPassiveInstance, R }. % Executes specified oneway on specified passive instance. -spec execute_oneway( passive_instance(), oneway_name(), method_arguments() ) -> passive_instance(). execute_oneway( PassiveInstance, OnewayName, OnewayArgs ) when is_record( PassiveInstance, ?passive_record ) andalso is_atom( OnewayName ) andalso is_list( OnewayArgs ) -> %trace_utils:trace_fmt( "Executing oneway ~s/~B on passive instance", % [ OnewayName, length( OnewayArgs ) ] ), { NewPassiveInstance, wooper_method_returns_void } = wooper_execute_method( OnewayName, OnewayArgs, PassiveInstance ), NewPassiveInstance; % Promote non-list argument to list: execute_oneway( PassiveInstance, OnewayName, OnewayArg ) when is_record( PassiveInstance, ?passive_record ) andalso is_atom( OnewayName ) -> %trace_utils:trace_fmt( "Executing oneway ~s on passive instance", % [ OnewayName ] ), { NewPassiveInstance, wooper_method_returns_void } = wooper_execute_method( OnewayName, [ OnewayArg ], PassiveInstance ), NewPassiveInstance. % Helpers. % Returns the state of a blank WOOPER instance of specified class. % % (helper) % -spec get_blank_state( classname() ) -> wooper:state(). get_blank_state( Classname ) -> #state_holder{ virtual_table=retrieve_virtual_table( Classname ), attribute_table= ?wooper_table_type:new( ?wooper_attribute_count_upper_bound ), actual_class=Classname, request_sender=undefined }. % Section for default handlers. % WOOPER default EXIT message handler; called if trapping EXIT signals. % % Returns an updated state. % % Can be overridden by defining or inheriting the onWOOPERExitReceived/3 oneway. % % (helper) % -spec default_exit_handler( basic_utils:pid_or_port(), basic_utils:exit_reason(), wooper:state() ) -> wooper:state(). default_exit_handler( PidOrPort, ExitReason, State ) -> log_warning( "WOOPER default EXIT handler of the ~w " "instance ~w ignored the following EXIT message " "from ~w:~n'~p'.", [ State#state_holder.actual_class, self(), PidOrPort, ExitReason ] ), State. % WOOPER default DOWN handler, for process monitors. % % Returns an updated state. % % Can be overridden by defining or inheriting the onWOOPERDownNotified/5 oneway. % % Note: not to be mixed up with the default_node_down_handler/3 / % onWOOPERNodeDisconnection/3 pair (which is node-related). % % (helper) % -spec default_down_handler( monitor_utils:monitor_reference(), monitor_utils:monitored_element_type(), monitor_utils:monitored_element(), basic_utils:exit_reason(), wooper:state() ) -> wooper:state(). default_down_handler( _MonitorReference, _MonitoredType, _MonitoredElement, _ExitReason=normal, State ) -> % Normal exits not notified: State; default_down_handler( MonitorReference, MonitoredType, MonitoredElement, ExitReason, State ) -> log_warning( "WOOPER default DOWN handler of the ~w " "instance ~w ignored the following down notification " "'~s' for monitored element ~p of type '~p' " "(monitor reference: ~w).", [ State#state_holder.actual_class, self(), ExitReason, MonitoredElement, MonitoredType, MonitorReference ] ), State. % WOOPER default node up handler. % % Returns an updated state. % % Can be overridden by defining or inheriting the onWOOPERNodeConnection/3 % oneway. % % (helper) % -spec default_node_up_handler( net_utils:atom_node_name(), monitor_utils:monitor_node_info(), wooper:state() ) -> wooper:state(). default_node_up_handler( Node, MonitorNodeInfo, State ) -> log_warning( "WOOPER default node up handler of the ~w " "instance ~w ignored the connection notification " "for node '~s' (information: ~p).", [ State#state_holder.actual_class, self(), Node, MonitorNodeInfo ] ), State. % WOOPER default node down handler. % % Returns an updated state. % % Can be overridden by defining or inheriting the onWOOPERNodeDisconnection/3 % oneway. % % Note: not to be mixed up with the default_down_handler/5 / % onWOOPERDownNotified/5 pair (which is process-related). % -spec default_node_down_handler( net_utils:atom_node_name(), monitor_utils:monitor_node_info(), wooper:state() ) -> wooper:state(). default_node_down_handler( Node, MonitorNodeInfo, State ) -> log_warning( "WOOPER default node down handler of the ~w " "instance ~w ignored the disconnection notification " "for node '~s' (information: ~p).", [ State#state_holder.actual_class, self(), Node, MonitorNodeInfo ] ), State. % Returns the virtual table corresponding to the specified class. % % (helper) % -spec retrieve_virtual_table( classname() ) -> ?wooper_table_type:?wooper_table_type(). -if( ?wooper_enable_otp_integration =:= true ). retrieve_virtual_table( Classname ) -> % The OTP way: wooper_class_manager:get_table( Classname ). -elif( ?wooper_enable_otp_integration =:= false ). retrieve_virtual_table( Classname ) -> % For per-instance virtual table: wooper_create_method_table_for(?MODULE). % The non-OTP way: wooper_class_manager:get_manager() ! { get_table, Classname, self() }, receive { wooper_virtual_table, Table } -> %?wooper_table_type:display( Table ), Table end. -endif. % wooper_enable_otp_integration % Triggers specified construction error (notify and throw). % % (helper) % -spec trigger_error( basic_utils:exception_class(), term(), classname(), [ method_arguments() ], code_utils:stack_trace() ) -> no_return(). trigger_error( _Reason, _ErrorTerm=undef, Classname, ConstructionParameters, _Stacktrace=[ _UndefCall={ ModuleName, FunctionName, UndefArgs, _Loc } | _ ] ) -> % An undef error is difficult to investigate (multiple possible reasons % behind), let's be nice to the developer: UndefArity = length( UndefArgs ), Diagnosis = code_utils:interpret_undef_exception( ModuleName, FunctionName, UndefArity ), Arity = length( ConstructionParameters ) + 1, log_error( "~nWOOPER error for PID ~w, " "constructor (~s:construct/~B) failed due to an 'undef' " "call to ~s:~s/~B; diagnosis: ~s.", [ self(), Classname, Arity, ModuleName, FunctionName, UndefArity, Diagnosis ] ), throw( { wooper_constructor_failed, self(), Classname, Arity, { undef, { ModuleName, FunctionName, UndefArity } } } ); trigger_error( Reason, ErrorTerm, Classname, ConstructionParameters, Stacktrace ) -> % Construction failed: % (error term would often be unreadable with ~p) Arity = length( ConstructionParameters ) + 1, log_error( "~nWOOPER error for PID ~w, " "constructor (~s:construct/~B) failed (cause: ~p):~n~n" " - with error term:~n ~p~n~n" " - stack trace was (latest calls first):~n~s~n" " - for construction parameters:~n ~p~n", [ self(), Classname, Arity, Reason, ErrorTerm, code_utils:interpret_stacktrace( Stacktrace ), ConstructionParameters ] ), throw( { wooper_constructor_failed, self(), Classname, Arity, ConstructionParameters, ErrorTerm } ). % Methods for getting information about an instance. % Returns the actual classname of the specified instance. % % Can be trusted. % % (helper) % -spec get_classname( wooper:state() ) -> classname(). get_classname( State ) -> State#state_holder.actual_class. % Returns the (user-level) attributes known of WOOPER for the specified state % (i.e. all attributes except the ones used internally by WOOPER). % % (helper) % -spec get_attribute_pairs( wooper:state() ) -> [ attribute_entry() ]. get_attribute_pairs( State ) -> AllAttrs = get_all_attributes( State ), ReservedAttrs = get_wooper_reserved_attribute_names(), % Remove WOOPER internals: filter_wooper_attributes( AllAttrs, ReservedAttrs, _Acc=[] ). % Removes from the specified atttributes the ones used internally by WOOPER (so % that only class-specific ones remain). % % (internal helper) % filter_wooper_attributes( _AttrPairs=[], _ReservedAttrs, Acc ) -> Acc; filter_wooper_attributes( _AttrPairs=[ AttrEntry={ Name, _Value } | T ], ReservedAttrs, Acc ) -> case lists:member( Name, ReservedAttrs ) of true -> filter_wooper_attributes( T, ReservedAttrs, Acc ); false -> filter_wooper_attributes( T, ReservedAttrs, [ AttrEntry | Acc ] ) end. % Returns a list of the attribute names that are used internally by WOOPER. -spec get_wooper_reserved_attribute_names() -> [ attribute_name() ]. get_wooper_reserved_attribute_names() -> []. % Returns a textual representation of the attributes of the specified state. -spec state_to_string( wooper:state() ) -> string(). state_to_string( State ) -> % Not using get_attribute_pairs/1 to rely on the full state: Attributes = get_all_attributes( State ), % We prefer having the attributes sorted by their name, in alphabetical % order: % SortedAttributes = lists:keysort( _Index=1, Attributes ), lists:foldl( fun( { AttName, AttrValue }, Acc ) -> Acc ++ io_lib:format( " * ~s = ~s~n", [ text_utils:term_to_string( AttName ), text_utils:term_to_string( AttrValue, _MaxDepth=16, _MaxLength=100 ) ] ) end, io_lib:format( "State of ~w:~nInstance of ~s with ~B attribute(s):~n", [ self(), get_classname( State ), length( Attributes ) ] ), SortedAttributes ). % Returns the source filename associated to specified class. % % Ex: get_class_filename( 'class_Foo' ) returns simply "class_Foo.erl". % -spec get_class_filename( classname() ) -> file_utils:filename(). get_class_filename( Classname ) -> text_utils:format( "~s.erl", [ Classname ] ). % Returns the time-out to be used for synchronous operations, depending on the % debug mode. % -spec get_synchronous_time_out( boolean() ) -> time_utils:time_out(). get_synchronous_time_out( _IsDebugMode=true ) -> % Suitable for most applications (5 seconds, to benefit from earlier % reports): 5000; get_synchronous_time_out( _IsDebugMode=false ) -> % Better for applications in production (30 minutes): 30*60*1000. -ifdef(wooper_debug). % Returns a textual representation of the virtual table corresponding to the % specified state. % % (helper) % -spec virtual_table_to_string( wooper:state() ) -> string(). virtual_table_to_string( State ) -> lists:foldl( fun( { { Name, Arity }, Module }, String ) -> String ++ io_lib:format( " * ~s/~B -> ~s~n", [ Name, Arity, Module ] ) end, _Acc=io_lib:format( "Virtual table of ~w:~n(method name/arity -> " "module defining that method)~n", [ self() ] ), _List=?wooper_table_type:enumerate( State#state_holder.virtual_table ) ). % Returns a textual representation of this instance, including its state and % virtual table. % % (helper) % -spec instance_to_string( wooper:state() ) -> string(). instance_to_string( State ) -> io_lib:format( "Inspection of instance ~w:~n~n + ~s~n + ~s", [ self(), state_to_string( State ), virtual_table_to_string( State ) ] ). % Displays the inner state of this instance. % % This is not a method. % -spec display_state( wooper:state() ) -> void(). display_state( State ) -> error_logger:info_msg( "~s~n", [ state_to_string( State ) ] ). % Displays the virtual table of this instance. % % This is not a method. % -spec display_virtual_table( wooper:state() ) -> void(). display_virtual_table( State ) -> error_logger:info_msg( "~s~n", [ virtual_table_to_string( State ) ] ). % Displays information about this instance. % % This is not a method. % -spec display_instance( wooper:state() ) -> void(). display_instance( State ) -> error_logger:info_msg( "~s~n", [ instance_to_string( State ) ] ). -endif. % wooper_debug % Returns all the attributes of this instance, as a list of { AttributeName, % AttributeValue } pairs. % -spec get_all_attributes( wooper:state() ) -> [ attribute_entry() ]. get_all_attributes( State ) -> ?wooper_table_type:enumerate( State#state_holder.attribute_table ). % Declares automatically the relevant BEAM directories in the code path, so that % Ceylan-WOOPER can be fully usable from then on. % % Note: % % - the code_utils.beam module of Ceylan-Myriad must be available from the % current code path % % - the CEYLAN_MYRIAD and CEYLAN_WOOPER environment variables must be defined % and must point to the respective root directories % % - the determined directories are not specifically checked for existence, % and are added at the end of the code path. % -spec declare_beam_dirs_for_wooper() -> void(). declare_beam_dirs_for_wooper() -> code_utils:declare_beam_dirs_for_myriad(), code_utils:declare_beam_dirs_for( "CEYLAN_WOOPER" ). % Log section. % Reports (on a best-effort basis) the specified information to the user, % typically by displaying an information report on the console. % -spec log_info( string() ) -> void(). log_info( String ) -> error_logger:info_msg( String ++ "\n" ). % Reports (on a best-effort basis) the specified information to the user, % typically by displaying an information report on the console. % -spec log_info( text_utils:format_string(), [ term() ] ) -> void(). log_info( FormatString, ValueList ) -> error_logger:info_msg( FormatString ++ "\n", ValueList ). % Reports (on a best-effort basis) the specified warning to the user, % typically by displaying a warning report on the console. % -spec log_warning( string() ) -> void(). log_warning( String ) -> error_logger:warning_msg( String ++ "\n" ), % Wait a bit, as error_msg seems asynchronous: system_utils:await_output_completion( ?wooper_warning_display_waiting ). % Reports (on a best-effort basis) the specified warning to the user, % typically by displaying a warning report on the console. % -spec log_warning( text_utils:format_string(), [ term() ] ) -> void(). log_warning( FormatString, ValueList ) -> error_logger:warning_msg( FormatString ++ "\n", ValueList ), % Wait a bit, as error_msg seems asynchronous: system_utils:await_output_completion( ?wooper_warning_display_waiting ). % Reports (as synchronously as possible, in order to avoid loosing this % notification) the specified error to the user, typically by displaying an % error report on the console (non-halting function, ex: no exception thrown). % -spec log_error( string() ) -> void(). log_error( Message ) -> error_logger:error_msg( Message ++ "\n" ), % Wait a bit, as error_msg seems asynchronous: system_utils:await_output_completion( ?wooper_error_display_waiting ). % Reports (as synchronously as possible, in order to avoid loosing this % notification) the specified error to the user, typically by displaying an % error report on the console (non-halting function, ex: no exception thrown). % -spec log_error( text_utils:format_string(), [ term() ] ) -> void(). log_error( FormatString, ValueList ) -> error_logger:error_msg( FormatString ++ "~n=END OF WOOPER ERROR REPORT FOR ~w ===~n~n~n", ValueList ++ [ self() ] ), % Wait a bit, as error_msg seems asynchronous: system_utils:await_output_completion( ?wooper_error_display_waiting ). % Reports (as synchronously as possible, in order to avoid loosing this % notification) the specified error about the current WOOPER instance % (preferably thanks to its state, otherwise with the current executed module, % so with fewer information) to the user, typically by displaying an error % report on the console (non-halting function, ex: no exception thrown). % -spec log_error( text_utils:format_string(), [ term() ], wooper:state() | basic_utils:module_name() ) -> void(). log_error( FormatString, ValueList, State ) when is_record( State, state_holder ) -> io:format( "~n", [] ), % Node information would be uselessly distracting: %log_error( "WOOPER error for ~s instance of PID ~w on node ~s: " % ++ FormatString, % [ State#state_holder.actual_class, self(), % node() | ValueList ] ); log_error( "WOOPER error for ~s instance of PID ~w: " ++ FormatString, [ State#state_holder.actual_class, self() | ValueList ] ); log_error( FormatString, ValueList, ModuleName ) when is_atom( ModuleName ) -> io:format( "~n", [] ), % Node information would be uselessly distracting: %log_error( "WOOPER error for instance of PID ~w on node ~s triggered " % "in module ~s: " ++ FormatString, % [ self(), ModuleName, node() | ValueList ] ). log_error( "WOOPER error for instance of PID ~w triggered " "in module ~s: " ++ FormatString, [ self(), ModuleName | ValueList ] ). % Called by WOOPER whenever a request fails, to report it on the console and to % the caller, and have the process instance exit. % -spec on_failed_request( request_name(), method_arguments(), pid(), basic_utils:error_type(), basic_utils:error_term(), code_utils:stack_trace(), wooper:state() ) -> no_return(). on_failed_request( RequestName, ArgumentList, CallerPid, ErrorType, ErrorTerm, Stacktrace, State ) -> Arity = length( ArgumentList ) + 1, ModulePrefix = lookup_method_prefix( RequestName, Arity, State ), log_error( "request ~s~s/~B failed (cause: ~s):~n~n" " - with error term:~n ~p~n~n" " - stack trace was (latest calls first):~n~s~n" " - caller being process ~w~n~n" " - for request parameters:~n ~p~n", [ ModulePrefix, RequestName, Arity, ErrorType, ErrorTerm, code_utils:interpret_stacktrace( Stacktrace ), CallerPid, ArgumentList ], State ), % ArgumentList and actual method module not propagated back to the caller: ErrorReason = { request_failed, State#state_holder.actual_class, self(), RequestName, { ErrorType, ErrorTerm } }, CallerPid ! { wooper_error, ErrorReason }, % Investigating a transient case where no message other than request_failed % was output: % %timer:sleep( 1000 ), % We do not want a duplicate error message, yet we cannot use 'normal' as % linked processes would not be triggered: % exit( request_failed ). % Called by WOOPER whenever a oneway fails, to report it on the console and to % the caller, and have the process instance exit. % -spec on_failed_oneway( oneway_name(), method_arguments(), basic_utils:error_type(), basic_utils:error_term(), code_utils:stack_trace(), wooper:state() ) -> no_return(). on_failed_oneway( OnewayAtom, ArgumentList, ErrorType, ErrorTerm, Stacktrace, State ) -> Arity = length( ArgumentList ) + 1, ModulePrefix = lookup_method_prefix( OnewayAtom, Arity, State ), log_error( "oneway ~s~s/~B failed (cause: ~s):~n~n" " - with error term:~n ~p~n~n" " - stack trace was (latest calls first):~n~s~n" " - for oneway parameters:~n ~p~n", [ ModulePrefix, OnewayAtom, Arity, ErrorType, ErrorTerm, code_utils:interpret_stacktrace( Stacktrace ), ArgumentList ], State ), % No caller to notify, for oneways. % We do not want a duplicate error message, yet we cannot use % 'normal' as linked processes would not be triggered: % exit( oneway_failed ). % Looks up the module defining specified method, and returns a textual prefix % specifying it, if found. % % Used for error management, hence designed not to fail. % % (helper) % -spec lookup_method_prefix( method_name(), arity(), wooper:state() ) -> string(). lookup_method_prefix( MethodAtom, Arity, State ) -> try wooper_lookup_method( State, MethodAtom, Arity ) of { value, Module } -> text_utils:format( "~s:", [ Module ] ); key_not_found -> "" catch _:_ -> "" end. % Helper function to test requests. % % Allows to test from the shell an instance by sending it requests (hence % needing a receive, whereas the caller is the shell), and waiting for any kind % of message sent back. % % Returns the actual result or received value. % % Available even when debug mode is off. % -spec send_and_listen( instance_pid(), request_name(), method_arguments() ) -> term(). send_and_listen( InstancePid, RequestName, Arguments ) -> InstancePid ! { RequestName, Arguments, self() }, receive { wooper_result, Result } -> %trace_utils:debug_fmt( % "Result of call to '~w' with arguments '~w': ~s", % [ RequestName, Arguments, % text_utils:term_to_string( Result ) ] ), Result; Anything -> %trace_utils:debug_fmt( % "Answer to call to '~w' with arguments '~w': ~s", % [ RequestName, Arguments, % text_utils:term_to_string( Anything ) ] ), Anything end. % Returns the result corresponding to the first pending WOOPER request (the % latest sent one), or blocks. % % (helper) % -spec receive_result() -> request_result( any() ). receive_result() -> receive { wooper_result, R } -> R end. % Deletion-related section. % Deletes (asynchronously: "fire and forget") the WOOPER instance(s) potentially % stored in the specified attribute list. % % Sets the corresponding attribute(s) to 'undefined', returns an updated state. % % Ex: in a destructor: DeleteState = delete_any_instance_referenced_in( [ % first_pid_attr, second_pid_attr ], State ) or % delete_any_instance_referenced_in( my_pid_attr, State ). % % (helper) % -spec delete_any_instance_referenced_in( [ attribute_name() ], wooper:state() ) -> wooper:state(). delete_any_instance_referenced_in( _Attributes=[], State ) -> State; delete_any_instance_referenced_in( [ PidAttribute | T ], State ) -> NewState = case ?getAttr(PidAttribute) of undefined -> State; Pid when is_pid( Pid ) -> Pid ! delete, setAttribute( State, PidAttribute, undefined ) end, delete_any_instance_referenced_in( T, NewState ); delete_any_instance_referenced_in( PidAttribute, State ) -> case ?getAttr(PidAttribute) of undefined -> State; Pid when is_pid( Pid ) -> Pid ! delete, setAttribute( State, PidAttribute, undefined ) end. % Deletes (synchronously, in a parallel yet blocking manner) the WOOPER % instance(s) potentially stored in specified attribute list (a standalone % attribute may be specified as well). % % Sets the corresponding attribute(s) to 'undefined', returns an updated state. % % Ex: in a destructor: NewState = % delete_synchronously_any_instance_referenced_in( [ first_pid_attr, % second_pid_attr ], State ) or % delete_synchronously_any_instance_referenced_in( my_pid_attr, State ). % -spec delete_synchronously_any_instance_referenced_in( [ attribute_name() ] | attribute_name(), wooper:state() ) -> wooper:state(). delete_synchronously_any_instance_referenced_in( Attributes, State ) -> delete_synchronously_any_instance_referenced_in( Attributes, _PreTestLiveliness=false, State ). % Deletes safely (pre-testing whether the specified process still exists before % attempting to delete it, in order to avoid having to wait for a synchronous % time-out) and synchronously, in a parallel yet blocking manner, the WOOPER % instance(s) potentially stored in specified attribute list (a standalone % attribute may be specified as well). % % Sets the corresponding attribute(s) to 'undefined', returns an updated state. % % Ex: in a destructor: NewState = % safe_delete_synchronously_any_instance_referenced_in( [ first_pid_attr, % second_pid_attr ], State ) or % safe_delete_synchronously_any_instance_referenced_in( my_pid_attr, State ). % -spec safe_delete_synchronously_any_instance_referenced_in( [ attribute_name() ] | attribute_name(), wooper:state() ) -> wooper:state(). safe_delete_synchronously_any_instance_referenced_in( Attributes, State ) -> delete_synchronously_any_instance_referenced_in( Attributes, _PreTestLiveliness=true, State ). delete_synchronously_any_instance_referenced_in( _Attributes=[], _PreTestLiveliness, State ) -> State; delete_synchronously_any_instance_referenced_in( Attributes, PreTestLiveliness, State ) when is_list( Attributes ) -> % Triggers the deletion of selected instances: { TargetAttributes, TargetPids } = delete_pid_from( Attributes, PreTestLiveliness, State ), %trace_utils:debug_fmt( % "delete_synchronously_any_instance_referenced_in:~n" % " - attributes are: ~p~n" % " - PIDs are: ~p~n", [ TargetAttributes, TargetPids ] ), % Waits for their completion: wait_for_deletion_ack( TargetPids ), %trace_utils:debug_fmt( "(all deletion acks received for ~p)", % [ TargetAttributes ] ), % Erases deleted PIDs: UndefinedAttributes = [ { AttrName, undefined } || AttrName <- TargetAttributes ], setAttributes( State, UndefinedAttributes ); delete_synchronously_any_instance_referenced_in( Attribute, PreTestLiveliness, State ) -> case ?getAttr( Attribute ) of undefined -> State; Pid when is_pid( Pid ) -> case PreTestLiveliness andalso not basic_utils:is_alive( Pid ) of % Only case where no actual deletion is needed: true -> %trace_utils:debug_fmt( % "(PID ~w was already dead, nothing done)", [ Pid ] ), ok; false -> %trace_utils:debug_fmt( "Sending sync delete to ~w.", % [ Pid ] ), Pid ! { synchronous_delete, self() }, receive { deleted, Pid } -> %trace_utils:debug_fmt( "Deletion of ~w confirmed.", % [ Pid ] ), ok end end, setAttribute( State, Attribute, undefined ) end. % Helper, which sends delete messages to all PIDs found in the list of % attributes, and returns a list of the attributes and a list of the PIDs. % % If PreTestLiveliness is true, checks first that the process is not already % dead, to avoid waiting for a synchronous time-out. % delete_pid_from( Attributes, PreTestLiveliness, State ) -> DeleteMessage = { synchronous_delete, self() }, delete_pid_from( Attributes, DeleteMessage, PreTestLiveliness, State, _AccAttr=[], _AccPid=[] ). delete_pid_from( _Attributes=[], _DeleteMessage, _PreTestLiveliness, _State, AccAttr, AccPid ) -> { AccAttr, AccPid }; delete_pid_from( [ Attr | T ], DeleteMessage, PreTestLiveliness, State, AccAttr, AccPid ) -> case ?getAttr( Attr ) of undefined -> delete_pid_from( T, DeleteMessage, PreTestLiveliness, State, AccAttr, AccPid ) ; Pid when is_pid( Pid ) -> case PreTestLiveliness andalso not basic_utils:is_alive( Pid ) of % Only case where no deletion oneway shall be sent: true -> delete_pid_from( T, DeleteMessage, PreTestLiveliness, State, [ Attr | AccAttr ], AccPid ); false -> %trace_utils:debug_fmt( "Deleting now ~s (PID: ~w).", % [ Attr, Pid ] ), Pid ! DeleteMessage, delete_pid_from( T, DeleteMessage, PreTestLiveliness, State, [ Attr | AccAttr ], [ Pid | AccPid ] ) end end. % Deletes specified instance synchronously. % % Will wait forever the effective termination of the specified instance. % -spec delete_synchronously_instance( instance_pid() ) -> void(). delete_synchronously_instance( InstancePid ) -> %trace_utils:debug_fmt( "delete_synchronously_instance for ~w.", % [ InstancePid ] ), InstancePid ! { synchronous_delete, self() }, receive { deleted, InstancePid } -> ok end. % Deletes specified instances synchronously (yet in parallel). % % Will wait forever the effective termination of all instances (and will % regularly write a message on the console if waiting for too long). % % (exported helper) % -spec delete_synchronously_instances( [ instance_pid() ] ) -> void(). delete_synchronously_instances( InstanceList ) -> %trace_utils:debug_fmt( "delete_synchronously_instances for ~p.", % [ InstanceList ] ), DeleteMessage = { synchronous_delete, self() }, [ I ! DeleteMessage || I <- InstanceList ], wait_for_deletion_ack( InstanceList ). % Helper used to wait for the receiving of all deletion acknowledgements: % % Could almost use basic_utils:wait_for_acks/3. % wait_for_deletion_ack( _WaitedPids=[] ) -> ok; wait_for_deletion_ack( WaitedPids ) -> receive { deleted, Pid } -> case lists:member( Pid, WaitedPids ) of false -> throw( { unexpected_pid_deletion, Pid } ); true -> NewWaitedPids = lists:delete( Pid, WaitedPids ), wait_for_deletion_ack( NewWaitedPids ) end % Note that this time-out is reset at each ack: after ?synchronous_time_out -> case examine_waited_deletions( WaitedPids, _Acc=[] ) of [] -> ok; NewWaitedPids -> trace_utils:debug_fmt( "(still waiting for the synchronous deletion of " "following live WOOPER instance(s): ~p)", [ NewWaitedPids ] ), % Warns, but does not trigger failures: wait_for_deletion_ack( NewWaitedPids ) end end. examine_waited_deletions( _WaitedPids=[], Acc ) -> Acc; examine_waited_deletions( _WaitedPids=[ Pid | T ], Acc ) -> %trace_utils:debug_fmt( "Testing whether ~p is alive...", [ Pid ] ), % Manages processes that are not local as well: case basic_utils:is_alive( Pid ) of true -> examine_waited_deletions( T, [ Pid | Acc ] ); false -> trace_utils:debug_fmt( "Stopped waiting for the deletion of instance " "whose PID is ~p: not found alive.", [ Pid ] ), examine_waited_deletions( T, Acc ) end. % Deletes specified instances synchronously (yet in parallel), safely, knowing % there might be duplicates in the specified list and that some instances may % even be already dead. % % Will wait forever the effective termination of all instances (and will % regularly write a message on the console if waiting for too long) . % % (exported helper) % -spec safe_delete_synchronously_instances( [ instance_pid() ] ) -> void(). safe_delete_synchronously_instances( InstanceList ) -> % Testing for liveliness allows to avoid synchronous time-outs: FilteredInstanceList = [ InstancePid || InstancePid <- list_utils:uniquify( InstanceList ), basic_utils:is_alive( InstancePid ) ], delete_synchronously_instances( FilteredInstanceList ). % Deletes specified passive instance. -spec delete_passive( passive_instance() ) -> void(). delete_passive( _PassiveInstance ) -> %trace_utils:trace( "Passive instance deleted." ), ok. % These functions are stubs, they shall never be called, as the WOOPER parse % transform is supposed to have replaced them at compilation-time. -spec return_state_result( any(), any() ) -> no_return(). return_state_result( _State, _Result ) -> throw( { untransformed_method_terminator, return_state_result } ). -spec return_state( any() ) -> no_return(). return_state( _State ) -> throw( { untransformed_method_terminator, return_state_result } ). -spec return_static( any() ) -> no_return(). return_static( _Value ) -> throw( { untransformed_method_terminator, return_static } ). -spec const_return_result( any() ) -> no_return(). const_return_result( _Value ) -> throw( { untransformed_method_terminator, const_return_result } ). -spec const_return() -> no_return(). const_return() -> throw( { untransformed_method_terminator, const_return } ). % Returns a set containing pairs whose first element is the name of a function % exported from the wooper module, and whose second element is its corresponding % arity (of course multiple functions might share the same name but rely on % different arities). % % Typically useful to better intercept user errors in method terminators. % -spec get_exported_functions_set() -> function_export_set(). get_exported_functions_set() -> set_utils:new( meta_utils:list_exported_functions( ?MODULE ) ).