aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lib/diameter/doc/src/diameter.xml353
-rw-r--r--lib/diameter/doc/src/diameter_app.xml28
-rw-r--r--lib/diameter/doc/src/diameter_sctp.xml3
-rw-r--r--lib/diameter/doc/src/diameter_transport.xml6
-rw-r--r--lib/diameter/src/base/diameter.erl2
-rw-r--r--lib/diameter/src/base/diameter_peer_fsm.erl289
-rw-r--r--lib/diameter/src/base/diameter_service.erl115
-rw-r--r--lib/diameter/src/base/diameter_watchdog.erl30
-rw-r--r--lib/diameter/test/diameter_dpr_SUITE.erl196
-rw-r--r--lib/diameter/test/diameter_traffic_SUITE.erl25
-rw-r--r--lib/diameter/test/modules.mk5
11 files changed, 800 insertions, 252 deletions
diff --git a/lib/diameter/doc/src/diameter.xml b/lib/diameter/doc/src/diameter.xml
index 80863f8eff..c93a7b2c67 100644
--- a/lib/diameter/doc/src/diameter.xml
+++ b/lib/diameter/doc/src/diameter.xml
@@ -512,6 +512,17 @@ following types.</p>
<taglist>
+<tag><c>start</c></tag>
+<tag><c>stop</c></tag>
+
+<item>
+<p>
+The service is being started or stopped.
+No event precedes a <c>start</c> event.
+No event follows a <c>stop</c> event and this event
+implies the termination of all transport processes.</p>
+</item>
+
<tag><c>{up, Ref, Peer, Config, Pkt}</c></tag>
<tag><c>{up, Ref, Peer, Config}</c></tag>
<tag><c>{down, Ref, Peer, Config}</c></tag>
@@ -606,6 +617,14 @@ indicated result code.
<c>Pkt</c> contains the CER in question.</p>
</item>
+<tag><c>{'CER', timeout}</c></tag>
+<item>
+<p>
+An expected CER was not received within <seealso
+marker="#capx_timeout">capx_timeout</seealso> of
+connection establishment.</p>
+</item>
+
<tag><c>{'CEA', Result, Caps, Pkt}</c></tag>
<item>
<code>
@@ -639,6 +658,14 @@ An incoming CEA contained errors and has been rejected.
<c>Pkt</c> contains the CEA in question.</p>
</item>
+<tag><c>{'CEA', timeout}</c></tag>
+<item>
+<p>
+An expected CEA was not received within <seealso
+marker="#capx_timeout">capx_timeout</seealso>
+of connection establishment.</p>
+</item>
+
</taglist>
</item>
@@ -693,7 +720,8 @@ well as the following.</p>
Defines a Diameter application supported by the service.</p>
<p>
-A service must configure one <c>application</c> for each Diameter
+A service must configure one <seealso
+marker="#application">application</seealso> for each Diameter
application it intends to support.
For an outgoing Diameter request, the relevant <c><seealso
marker="#application_alias">application_alias()</seealso></c> is
@@ -708,7 +736,7 @@ file.</p>
| node
| nodes
| [node()]
- | diameter:evaluable()}</c></tag>
+ | evaluable()}</c></tag>
<item>
<p>
Specifies the degree to which multiple transport connections to the
@@ -718,10 +746,10 @@ same peer are accepted by the service.</p>
If type <c>[node()]</c> then a connection is rejected if another already
exists on any of the specified nodes.
Values of type <c>false</c>, <c>node</c>, <c>nodes</c> or
-<c>diameter:evaluable()</c> are equivalent to values <c>[]</c>,
-<c>[node()]</c>, <c>[node()|nodes()]</c> and the evaluated value,
-respectively, evaluation of each expression taking place whenever a
-new connection is to be established.
+<seealso marker="#evaluable">evaluable()</seealso> are equivalent to
+values <c>[]</c>, <c>[node()]</c>, <c>[node()|nodes()]</c> and the
+evaluated value, respectively, evaluation of each expression taking
+place whenever a new connection is to be established.
Note that <c>false</c> allows an unlimited number of connections to be
established with the same peer.</p>
@@ -734,14 +762,14 @@ Defaults to <c>nodes</c>.</p>
</item>
<tag><c>{sequence, {H,N} | <seealso
- marker="diameter#evaluable">diameter:evaluable()</seealso>}</c></tag>
+ marker="#evaluable">evaluable()</seealso>}</c></tag>
<item>
<p>
Specifies a constant value <c>H</c> for the topmost <c>32-N</c> bits of
of 32-bit End-to-End and Hop-by-Hop identifiers generated
by the service, either explicity or as a return value of a function
to be evaluated at <seealso
-marker="diameter#start_service">diameter:start_service/2</seealso>.
+marker="#start_service">start_service/2</seealso>.
In particular, an identifier <c>Id</c> is mapped to a new identifier
as follows.</p>
<code>
@@ -775,53 +803,7 @@ marker="#add_transport">add_transport/2</seealso>.
Has one of the following types.</p>
<taglist>
-<tag><c>{transport_module, atom()}</c></tag>
-<item>
-<p>
-A module implementing a transport process as defined in <seealso
-marker="diameter_transport">diameter_transport(3)</seealso>.
-Defaults to <c>diameter_tcp</c> if unspecified.</p>
-
-<p>
-Multiple <c>transport_module</c> and <c>transport_config</c>
-options are allowed.
-The order of these is significant in this case (and only in this case),
-a <c>transport_module</c> being paired with the first
-<c>transport_config</c> following it in the options list, or the
-default value for trailing modules.
-Transport starts will be attempted with each of the
-modules in order until one establishes a connection within the
-corresponding timeout (see below) or all fail.</p>
-</item>
-
-<tag><c>{transport_config, term()}</c></tag>
-<tag><c>{transport_config, term(), <seealso marker="diameter_dict#DATA_TYPES">Unsigned32()</seealso>}</c></tag>
-<item>
-<p>
-A term passed as the third argument to the <seealso
-marker="diameter_transport#start">start/3</seealso> function of
-the relevant <c>transport_module</c> in order to start a transport process.
-Defaults to the empty list if unspecified.</p>
-
-<p>
-The 3-tuple form additionally specifies an interval, in milliseconds,
-after which a started transport process should be terminated if it has
-not yet established a connection.
-For example, the following options on a connecting transport
-request a connection with one peer over SCTP or another
-(typically the same) over TCP.</p>
-
-<code>
-{transport_module, diameter_sctp}
-{transport_config, SctpOpts, 5000}
-{transport_module, diameter_tcp}
-{transport_config, TcpOpts}
-</code>
-
-<p>
-To listen on both SCTP and TCP, define one transport for each.</p>
-</item>
-
+<marker id="applications"/>
<tag><c>{applications, [<seealso marker="#application_alias">application_alias()</seealso>]}</c></tag>
<item>
<p>
@@ -831,6 +813,7 @@ Defaults to all applications configured on the service in question.
Applications not configured on the service in question are ignored.</p>
</item>
+<marker id="capabilities"/>
<tag><c>{capabilities, [<seealso marker="#capability">capability()</seealso>]}</c></tag>
<item>
<p>
@@ -845,56 +828,156 @@ TLS is desired over TCP as implemented by
<seealso marker="diameter_tcp">diameter_tcp(3)</seealso>.</p>
</item>
+<marker id="capabilities_cb"/>
<tag><c>{capabilities_cb, <seealso marker="#evaluable">evaluable()</seealso>}</c></tag>
<item>
<p>
A callback invoked upon reception of CER/CEA during capabilities
exchange in order to ask whether or not the connection should
be accepted.
-Applied to the relevant <c><seealso
-marker="#transport_ref">transport_ref()</seealso></c> and the
-<c>#diameter_caps{}</c> record of the connection.
-Returning <c>ok</c> accepts the connection.
-Returning <c>integer()</c> causes an incoming
-CER to be answered with the specified Result-Code.
-Returning <c>discard</c> causes an incoming CER to
-be discarded.
-Returning <c>unknown</c> is equivalent to returning <c>3010</c>,
-DIAMETER_UNKNOWN_PEER.
-Returning anything but <c>ok</c> or a 2xxx series result
-code causes the transport connection to be broken.</p>
+Applied to the <c><seealso
+marker="#transport_ref">transport_ref()</seealso></c> and
+<c>#diameter_caps{}</c> record of the connection.</p>
+
+<p>
+The return value can have one of the following types.</p>
+<taglist>
+<tag><c>ok</c></tag>
+<item>
<p>
-Multiple <c>capabilities_cb</c> options can be specified, in which
+Accept the connection.</p>
+</item>
+
+<tag><c>integer()</c></tag>
+<item>
+<p>
+Causes an incoming CER to be answered with the specified Result-Code.</p>
+</item>
+
+<tag><c>discard</c></tag>
+<item>
+<p>
+Causes an incoming CER to be discarded without CEA being sent.</p>
+</item>
+
+<tag><c>unknown</c></tag>
+<item>
+<p>
+Equivalent to returning <c>3010</c>, DIAMETER_UNKNOWN_PEER.</p>
+</item>
+</taglist>
+
+<p>
+Returning anything but <c>ok</c> or a 2xxx series result
+code causes the transport connection to be broken.
+Multiple <seealso marker="#capabilities_cb">capabilities_cb</seealso>
+options can be specified, in which
case the corresponding callbacks are applied until either all return
<c>ok</c> or one does not.</p>
+</item>
-<marker id="watchdog_timer"/>
+<marker id="capx_timeout"/>
+<tag><c>{capx_timeout,
+ <seealso marker="diameter_dict#DATA_TYPES">Unsigned32()</seealso>}</c></tag>
+<item>
+<p>
+The number of milliseconds after which a transport process having an
+established transport connection will be terminated if the expected
+capabilities exchange message (CER or CEA) is not received from the peer.
+For a connecting transport, the timing reconnection attempts is
+governed by <seealso marker="#watchdog_timer">watchdog_timer</seealso> or
+<seealso marker="#reconnect_timer">reconnect_timer</seealso> expiry.
+For a listening transport, the peer determines the timing.</p>
+
+<p>
+Defaults to 10000.</p>
</item>
-<tag><c>{watchdog_timer, TwInit}</c></tag>
+<marker id="disconnect_cb"/>
+<tag><c>{disconnect_cb, <seealso marker="#evaluable">evaluable()</seealso>}</c></tag>
+
<item>
-<code>
-TwInit = <seealso marker="diameter_dict#DATA_TYPES">Unsigned32()</seealso>
- | {M,F,A}
-</code>
+<p>
+A callback invoked prior to terminating the transport process of a
+transport connection having watchdog state <c>OKAY</c>.
+Applied to <c>Reason=transport|service|application</c> and the
+<c><seealso marker="#transport_ref">transport_ref()</seealso></c> and
+<c><seealso marker="diameter_app#peer">diameter_app:peer()</seealso></c>
+in question, <c>Reason</c> indicating whether the the diameter
+application is being stopped, the service in question is being stopped
+at <seealso
+marker="#stop_service">stop_service/1</seealso> or
+the transport in question is being removed at <seealso
+marker="#remove_transport">remove_transport/2</seealso>,
+respectively.</p>
<p>
-The RFC 3539 watchdog timer.
-An integer value is interpreted as the RFC's TwInit in milliseconds,
-a jitter of &plusmn; 2 seconds being added at each rearming of the
-timer to compute the RFC's Tw.
-An MFA is expected to return the RFC's Tw directly, with jitter
-applied, allowing the jitter calculation to be performed by
-the callback.</p>
+The return value can have one of the following types.</p>
+<taglist>
+<tag><c>{dpr, [option()]}</c></tag>
+<item>
<p>
-An integer value must be at least 6000 as required by RFC 3539.
-Defaults to 30000 if unspecified.</p>
+Causes Disconnect-Peer-Request to be sent to the peer, the transport
+process being terminated following reception of
+Disconnect-Peer-Answer or timeout.
+An <c>option()</c> can be one of the following.</p>
-<marker id="reconnect_timer"/>
+<taglist>
+<tag><c>{cause, 0|rebooting|1|busy|2|goaway}</c></tag>
+<item>
+<p>
+The Disconnect-Cause to send, <c>REBOOTING</c>, <c>BUSY</c> and
+<c>DO_NOT_WANT_TO_TALK_TO_YOU</c> respectively.
+Defaults to <c>rebooting</c> for <c>Reason=service|application</c> and
+<c>goaway</c> for <c>Reason=transport</c>.</p>
+</item>
+
+<tag><c>{timeout,
+ <seealso marker="diameter_dict#DATA_TYPES">Unsigned32()</seealso>}</c></tag>
+<item>
+<p>
+The number of milliseconds after which the transport process is
+terminated if DPA has not been received.
+Defaults to 1000.</p>
+</item>
+</taglist>
+</item>
+
+<tag><c>dpr</c></tag>
+<item>
+<p>
+Equivalent to <c>{dpr, []}</c>.</p>
+</item>
+
+<tag><c>close</c></tag>
+<item>
+<p>
+Causes the transport process to be terminated without
+Disconnect-Peer-Request being sent to the peer.</p>
+</item>
+
+<tag><c>ignore</c></tag>
+<item>
+<p>
+Equivalent to not having configured the callback.</p>
+</item>
+</taglist>
+
+<p>
+Multiple <seealso marker="#disconnect_cb">disconnect_cb</seealso>
+options can be specified, in which
+case the corresponding callbacks are applied until one of them returns
+a value other than <c>ignore</c>.
+All callbacks returning <c>ignore</c> is equivalent to not having
+configured them.</p>
+
+<p>
+Defaults to a single callback returning <c>dpr</c>.</p>
</item>
+<marker id="reconnect_timer"/>
<tag><c>{reconnect_timer, Tc}</c></tag>
<item>
<code>
@@ -906,8 +989,9 @@ For a connecting transport, the RFC 3588 Tc timer, in milliseconds.
Note that this timer determines the frequency with which a transport
will attempt to establish a connection with its peer only <em>before</em>
an initial connection is established: once there is an initial
-connection it's watchdog_timer that determines the frequency of
-reconnection attempts, as required by RFC 3539.</p>
+connection it's <seealso
+marker="#watchdog_timer">watchdog_timer</seealso> that determines the
+frequency of reconnection attempts, as required by RFC 3539.</p>
<p>
For a listening transport, the timer specifies the time after which a
@@ -915,14 +999,89 @@ previously connected peer will be forgotten: a connection after this time is
regarded as an initial connection rather than a reestablishment,
causing the RFC 3539 state machine to pass to state OKAY rather than
REOPEN.
-Note that these semantics are not goverened by the RFC and
-that a listening transport's <c>reconnect_timer</c> should be greater
+Note that these semantics are not governed by the RFC and
+that a listening transport's <seealso
+marker="#reconnect_timer">reconnect_timer</seealso> should be greater
than its peer's Tw plus jitter.</p>
<p>
Defaults to 30000 for a connecting transport and 60000 for a listening
transport.</p>
+</item>
+
+<marker id="transport_config"/>
+<tag><c>{transport_config, term()}</c></tag>
+<tag><c>{transport_config, term(), <seealso marker="diameter_dict#DATA_TYPES">Unsigned32()</seealso>}</c></tag>
+<item>
+<p>
+A term passed as the third argument to the <seealso
+marker="diameter_transport#start">start/3</seealso> function of
+the relevant <seealso
+marker="#transport_module">transport_module</seealso> in order to
+start a transport process.
+Defaults to the empty list if unspecified.</p>
+
+<p>
+The 3-tuple form additionally specifies an interval, in milliseconds,
+after which a started transport process should be terminated if it has
+not yet established a connection.
+For example, the following options on a connecting transport
+request a connection with one peer over SCTP or another
+(typically the same) over TCP.</p>
+<code>
+{transport_module, diameter_sctp}
+{transport_config, SctpOpts, 5000}
+{transport_module, diameter_tcp}
+{transport_config, TcpOpts}
+</code>
+
+<p>
+To listen on both SCTP and TCP, define one transport for each.</p>
+</item>
+
+<marker id="transport_module"/>
+<tag><c>{transport_module, atom()}</c></tag>
+<item>
+<p>
+A module implementing a transport process as defined in <seealso
+marker="diameter_transport">diameter_transport(3)</seealso>.
+Defaults to <c>diameter_tcp</c> if unspecified.</p>
+
+<p>
+Multiple <c>transport_module</c> and <seealso
+marker="#transport_config">transport_config</seealso>
+options are allowed.
+The order of these is significant in this case (and only in this case),
+a <c>transport_module</c> being paired with the first
+<seealso marker="#transport_config">transport_config</seealso>
+following it in the options list, or the default value for trailing
+modules.
+Transport starts will be attempted with each of the
+modules in order until one establishes a connection within the
+corresponding timeout (see below) or all fail.</p>
+</item>
+
+<marker id="watchdog_timer"/>
+<tag><c>{watchdog_timer, TwInit}</c></tag>
+<item>
+<code>
+TwInit = <seealso marker="diameter_dict#DATA_TYPES">Unsigned32()</seealso>
+ | {M,F,A}
+</code>
+
+<p>
+The RFC 3539 watchdog timer.
+An integer value is interpreted as the RFC's TwInit in milliseconds,
+a jitter of &plusmn; 2 seconds being added at each rearming of the
+timer to compute the RFC's Tw.
+An MFA is expected to return the RFC's Tw directly, with jitter
+applied, allowing the jitter calculation to be performed by
+the callback.</p>
+
+<p>
+An integer value must be at least 6000 as required by RFC 3539.
+Defaults to 30000 if unspecified.</p>
</item>
</taglist>
@@ -1150,7 +1309,7 @@ at the time the diameter application was started.</p>
<!-- ===================================================================== -->
<func>
-<name>remove_transport(SvcName, Pred) -> ok</name>
+<name>remove_transport(SvcName, Pred) -> ok | {error, Reason}</name>
<fsummary>Remove previously added transports.</fsummary>
<type>
<v>SvcName = <seealso marker="#service_name">service_name()</seealso></v>
@@ -1160,6 +1319,7 @@ at the time the diameter application was started.</p>
<v>&nbsp;&nbsp;&nbsp; | fun((<seealso marker="#transport_ref">transport_ref()</seealso>, list()) -> boolean())</v>
<v>&nbsp;&nbsp;&nbsp; | fun((list()) -> boolean())</v>
<v>MFA = {atom(), atom(), list()}</v>
+<v>Reason = term()</v>
</type>
<desc>
<p>
@@ -1185,15 +1345,12 @@ Pred = {M,F,A}: fun(Ref, Type, Opts) -> apply(M, F, [Ref, Type, Opts | A]) end
</code>
<p>
-Removing a transport causes all associated transport connections to
-be broken.
-A DPR message with
-Disconnect-Cause <c>DO_NOT_WANT_TO_TALK_TO_YOU</c> will be sent
-to each connected peer before disassociating the transport configuration
-from the service and terminating the transport upon reception of
-DPA or timeout.</p>
-
-<!-- TODO: document the timeout value, possibly make configurable. -->
+Removing a transport causes the corresponding transport processes to
+be terminated.
+Whether or not a DPR message is sent to a peer is
+controlled by
+value of <seealso marker="disconnect_cb">disconnect_cb</seealso>
+configured on the transport.</p>
<marker id="service_info"/>
</desc>
diff --git a/lib/diameter/doc/src/diameter_app.xml b/lib/diameter/doc/src/diameter_app.xml
index 9d8a6568eb..b6870f7c28 100644
--- a/lib/diameter/doc/src/diameter_app.xml
+++ b/lib/diameter/doc/src/diameter_app.xml
@@ -309,12 +309,12 @@ by either <seealso marker="#handle_answer">handle_answer/4</seealso>
or <seealso marker="#handle_error">handle_error/4</seealso> depending
on whether or not an answer message is received from the peer.
If the transport becomes unavailable after <seealso
-marker="prepare_request">prepare_request/3</seealso> then a new <seealso
+marker="#prepare_request">prepare_request/3</seealso> then a new <seealso
marker="#pick_peer">pick_peer/4</seealso> callback may take place to
failover to an alternate peer, after which <seealso
marker="#prepare_retransmit">prepare_retransmit/3</seealso> takes the
place of <seealso
-marker="prepare_request">prepare_request/3</seealso> in resending the
+marker="#prepare_request">prepare_request/3</seealso> in resending the
request.
There is no guarantee that a <seealso
marker="#pick_peer">pick_peer/4</seealso> callback to select
@@ -382,7 +382,7 @@ communicate transport (or any other) data to the callback.</p>
<p>
A returned <seealso marker="#packet">packet()</seealso> can set
the <c>header</c> field to a
-<c>#diameter_header{}</c> in order to specify values that should
+<c>#diameter_header{}</c> to specify values that should
be preserved in the outgoing request, values otherwise being those in
the header record contained in <c>Packet</c>.
A returned <c>length</c>, <c>cmd_code</c> or <c>application_id</c> is
@@ -537,7 +537,8 @@ not selected.</p>
| {relay, [Opt]}
| discard
| {eval|eval_packet, Action, PostF}</v>
-<v>Reply = {reply, <seealso marker="#message">message()</seealso>}
+<v>Reply = {reply, <seealso marker="#packet">packet()</seealso>
+ | <seealso marker="#message">message()</seealso>}
| {protocol_error, 3000..3999}</v>
<v>Opt = <seealso marker="diameter#call_opt">diameter:call_opt()</seealso></v>
<v>PostF = <seealso marker="diameter#evaluable">diameter:evaluable()</seealso></v>
@@ -568,7 +569,7 @@ The argument <seealso marker="#packet">packet()</seealso> has the following sign
</code>
<p>
-The <c>msg</c> field will be <c>undefined</c> only in case the request has
+The <c>msg</c> field will be <c>undefined</c> in case the request has
been received in the relay application.
Otherwise it contains the record representing the request as outlined
in <seealso
@@ -590,19 +591,26 @@ the relay application.</p>
The <c>transport_data</c> field contains an arbitrary term passed into
diameter from the transport module in question, or the atom
<c>undefined</c> if the transport specified no data.
-The term is preserved in the <seealso marker="#packet">packet()</seealso> containing any answer message
-sent back to the transport process unless another value is explicitly
-specified.</p>
+The term is preserved if a <seealso
+marker="#packet">message()</seealso> is returned but must be set
+explicitly in a returned <seealso marker="#packet">packet()</seealso>.</p>
<p>
The semantics of each of the possible return values are as follows.</p>
<taglist>
-<tag><c>{reply, <seealso marker="#message">message()</seealso>}</c></tag>
+<tag><c>{reply, <seealso marker="#packet">packet()</seealso>
+ | <seealso marker="#message">message()</seealso>}</c></tag>
<item>
<p>
-Send the specified answer message to the peer.</p>
+Send the specified answer message to the peer.
+In the case of a <seealso marker="#packet">packet()</seealso>, the
+message to be sent must be set in the
+<c>msg</c> field and the <c>header</c> field can be set to a
+<c>#diameter_header{}</c> to specify values that should be
+preserved in the outgoing answer, appropriate values otherwise
+being set by diameter.</p>
</item>
<tag><c>{protocol_error, 3000..3999}</c></tag>
diff --git a/lib/diameter/doc/src/diameter_sctp.xml b/lib/diameter/doc/src/diameter_sctp.xml
index 955169349c..709b17c0d2 100644
--- a/lib/diameter/doc/src/diameter_sctp.xml
+++ b/lib/diameter/doc/src/diameter_sctp.xml
@@ -38,7 +38,8 @@ under the License.
<description>
<p>
-This module implements diameter transport over SCTP using gen_sctp.
+This module implements diameter transport over SCTP using <seealso
+marker="kernel:gen_sctp">gen_sctp</seealso>.
It can be specified as the value of a transport_module option to
<seealso
marker="diameter#add_transport">diameter:add_transport/2</seealso>
diff --git a/lib/diameter/doc/src/diameter_transport.xml b/lib/diameter/doc/src/diameter_transport.xml
index d9b36a1e09..0c8b41397a 100644
--- a/lib/diameter/doc/src/diameter_transport.xml
+++ b/lib/diameter/doc/src/diameter_transport.xml
@@ -149,9 +149,9 @@ contains the binary to send.</p>
<tag><c>{diameter, {close, Pid}}</c></tag>
<item>
<p>
-A request to close the transport connection.
-The transport process should terminate after closing the
-connection.
+A request to terminate the transport process after having received DPA
+in response to DPR.
+The transport process should exit.
<c>Pid</c> is the pid() of the parent process.</p>
</item>
diff --git a/lib/diameter/src/base/diameter.erl b/lib/diameter/src/base/diameter.erl
index 3e3a6be0ef..8f9901907a 100644
--- a/lib/diameter/src/base/diameter.erl
+++ b/lib/diameter/src/base/diameter.erl
@@ -330,6 +330,8 @@ call(SvcName, App, Message) ->
| {applications, [app_alias()]}
| {capabilities, [capability()]}
| {capabilities_cb, evaluable()}
+ | {capx_timeout, 'Unsigned32'()}
+ | {disconnect_cb, evaluable()}
| {watchdog_timer, 'Unsigned32'() | {module(), atom(), list()}}
| {reconnect_timer, 'Unsigned32'()}
| {private, any()}.
diff --git a/lib/diameter/src/base/diameter_peer_fsm.erl b/lib/diameter/src/base/diameter_peer_fsm.erl
index 3f4945f7a6..c4320fcb99 100644
--- a/lib/diameter/src/base/diameter_peer_fsm.erl
+++ b/lib/diameter/src/base/diameter_peer_fsm.erl
@@ -48,15 +48,19 @@
-include("diameter_internal.hrl").
-include("diameter_gen_base_rfc3588.hrl").
+%% Values of Disconnect-Cause in DPR.
-define(GOAWAY, ?'DIAMETER_BASE_DISCONNECT-CAUSE_DO_NOT_WANT_TO_TALK_TO_YOU').
-define(REBOOT, ?'DIAMETER_BASE_DISCONNECT-CAUSE_REBOOTING').
+-define(BUSY, ?'DIAMETER_BASE_DISCONNECT-CAUSE_BUSY').
-define(NO_INBAND_SECURITY, 0).
-define(TLS, 1).
%% Keys in process dictionary.
-define(CB_KEY, cb). %% capabilities callback
+-define(DPR_KEY, dpr). %% disconnect callback
-define(DWA_KEY, dwa). %% outgoing DWA
+-define(REF_KEY, ref). %% transport_ref()
-define(Q_KEY, q). %% transport start queue
-define(START_KEY, start). %% start of connected transport
-define(SEQUENCE_KEY, mask). %% mask for sequence numbers
@@ -68,28 +72,40 @@
%% A 2xxx series Result-Code. Not necessarily 2001.
-define(IS_SUCCESS(N), 2 == (N) div 1000).
+%% Guards.
+-define(IS_UINT32(N), (is_integer(N) andalso 0 =< N andalso 0 == N bsr 32)).
+-define(IS_TIMEOUT(N), ?IS_UINT32(N)).
+-define(IS_CAUSE(N), N == ?REBOOT; N == rebooting;
+ N == ?GOAWAY; N == goaway;
+ N == ?BUSY; N == busy).
+
%% RFC 3588:
%%
%% Timeout An application-defined timer has expired while waiting
%% for some event.
%%
-define(EVENT_TIMEOUT, 10000).
+%% Default timeout for reception of CER/CEA.
-%% How long to wait for a DPA in response to DPR before simply
-%% aborting. Used to distinguish between shutdown and not but there's
-%% not really any need. Stopping a service will require a timeout if
-%% the peer doesn't answer DPR so the value should be short-ish.
+%% Default timeout for DPA in response to DPR. A bit short but the
+%% timeout used to be hardcoded. (So it could be worse.)
-define(DPA_TIMEOUT, 1000).
+-type uint32() :: diameter:'Unsigned32'().
+
-record(state,
- {state = 'Wait-Conn-Ack' %% state of RFC 3588 Peer State Machine
- :: 'Wait-Conn-Ack' | recv_CER | 'Wait-CEA' | 'Open',
+ {state %% of RFC 3588 Peer State Machine
+ :: 'Wait-Conn-Ack' %% old code
+ | {'Wait-Conn-Ack', uint32()}
+ | recv_CER
+ | 'Wait-CEA' %% old code
+ | {'Wait-CEA', uint32(), uint32()}
+ | 'Open',
mode :: accept | connect | {connect, reference()},
- parent :: pid(),
- transport :: pid(),
+ parent :: pid(), %% watchdog process
+ transport :: pid(), %% transport process
service :: #diameter_service{},
- dpr = false :: false | {diameter:'Unsigned32'(),
- diameter:'Unsigned32'()}}).
+ dpr = false :: false | {uint32(), uint32()}}).
%% | hop by hop and end to end identifiers
%% There are non-3588 states possible as a consequence of 5.6.1 of the
@@ -163,19 +179,24 @@ i({WPid, Type, Opts, #diameter_service{} = Svc}) -> %% from old code
i({WPid, Type, Opts, {?NOMASK, [node() | nodes()], Svc}});
i({WPid, T, Opts, {Mask, Nodes, #diameter_service{applications = Apps,
- capabilities = Caps}
+ capabilities = LCaps}
= Svc}}) ->
[] /= Apps orelse ?ERROR({no_apps, T, Opts}),
- putr(?DWA_KEY, dwa(Caps)),
+ putr(?DWA_KEY, dwa(LCaps)),
{M, Ref} = T,
diameter_stats:reg(Ref),
- {[Ts], Rest} = proplists:split(Opts, [capabilities_cb]),
- putr(?CB_KEY, {Ref, [F || {_,F} <- Ts]}),
+ {[Cs,Ds], Rest} = proplists:split(Opts, [capabilities_cb, disconnect_cb]),
+ putr(?CB_KEY, {Ref, [F || {_,F} <- Cs]}),
+ putr(?DPR_KEY, [F || {_, F} <- Ds]),
+ putr(?REF_KEY, Ref),
putr(?SEQUENCE_KEY, Mask),
putr(?RESTRICT_KEY, Nodes),
erlang:monitor(process, WPid),
{TPid, Addrs} = start_transport(T, Rest, Svc),
- #state{parent = WPid,
+ Tmo = proplists:get_value(capx_timeout, Opts, ?EVENT_TIMEOUT),
+ ?IS_TIMEOUT(Tmo) orelse ?ERROR({invalid, {capx_timeout, Tmo}}),
+ #state{state = {'Wait-Conn-Ack', Tmo},
+ parent = WPid,
transport = TPid,
mode = M,
service = svc(Svc, Addrs)}.
@@ -188,8 +209,8 @@ i({WPid, T, Opts, {Mask, Nodes, #diameter_service{applications = Apps,
%% watchdog start (start/2) succeeds regardless so as not to crash the
%% service.
-start_transport(T, Opts, #diameter_service{capabilities = Caps} = Svc) ->
- Addrs0 = Caps#diameter_caps.host_ip_address,
+start_transport(T, Opts, #diameter_service{capabilities = LCaps} = Svc) ->
+ Addrs0 = LCaps#diameter_caps.host_ip_address,
start_transport(Addrs0, {T, Opts, Svc}).
start_transport(Addrs0, T) ->
@@ -212,9 +233,9 @@ svc(Svc, []) ->
svc(Svc, Addrs) ->
readdr(Svc, Addrs).
-readdr(#diameter_service{capabilities = Caps0} = Svc, Addrs) ->
- Caps = Caps0#diameter_caps{host_ip_address = Addrs},
- Svc#diameter_service{capabilities = Caps}.
+readdr(#diameter_service{capabilities = LCaps0} = Svc, Addrs) ->
+ LCaps = LCaps0#diameter_caps{host_ip_address = Addrs},
+ Svc#diameter_service{capabilities = LCaps}.
%% The 4-tuple Data returned from diameter_peer:start/1 identifies the
%% transport module/config use to start the transport process in
@@ -313,13 +334,17 @@ eraser(Key) ->
%% transition/2
+%% Started in old code.
+transition(T, #state{state = 'Wait-Conn-Ack' = PS} = S) ->
+ transition(T, S#state{state = {PS, ?EVENT_TIMEOUT}});
+
%% Connection to peer.
transition({diameter, {TPid, connected, Remote}},
#state{transport = TPid,
state = PS,
mode = M}
= S) ->
- 'Wait-Conn-Ack' = PS, %% assert
+ {'Wait-Conn-Ack', _} = PS, %% assert
connect = M, %%
keep_transport(TPid),
send_CER(S#state{mode = {M, Remote}});
@@ -331,11 +356,11 @@ transition({diameter, {TPid, connected}},
mode = M,
parent = Pid}
= S) ->
- 'Wait-Conn-Ack' = PS, %% assert
+ {'Wait-Conn-Ack', Tmo} = PS, %% assert
accept = M, %%
keep_transport(TPid),
Pid ! {accepted, self()},
- start_timer(S#state{state = recv_CER});
+ start_timer(Tmo, S#state{state = recv_CER});
%% Connection established after receiving a connection_timeout
%% message. This may be followed by an incoming message which arrived
@@ -349,7 +374,7 @@ transition({diameter, {_, connected, _}}, _) ->
%% Connection has timed out: start an alternate.
transition({connection_timeout = T, TPid},
#state{transport = TPid,
- state = 'Wait-Conn-Ack'}
+ state = {'Wait-Conn-Ack', _}}
= S) ->
exit(TPid, {shutdown, T}),
start_next(S);
@@ -364,7 +389,7 @@ transition({diameter, {recv, Pkt}}, S) ->
%% Timeout when still in the same state ...
transition({timeout, PS}, #state{state = PS}) ->
- stop;
+ {stop, {capx(PS), timeout}};
%% ... or not.
transition({timeout, _}, _) ->
@@ -375,25 +400,19 @@ transition({send, Msg}, #state{transport = TPid}) ->
send(TPid, Msg),
ok;
-%% Request for graceful shutdown.
-transition({shutdown, Pid}, #state{parent = Pid, dpr = false} = S) ->
- dpr(?GOAWAY, S);
-transition({shutdown, Pid}, #state{parent = Pid}) ->
- ok;
-
-%% Application shutdown.
-transition(shutdown, #state{dpr = false} = S) ->
- dpr(?REBOOT, S);
-transition(shutdown, _) -> %% DPR already send: ensure expected timeout
- dpa_timer(),
+%% Messages from old (diameter_service) code.
+transition(shutdown = T, #state{parent = Pid} = S) ->
+ transition({T, Pid, service}, S); %% Reason irrelevant: old code has no cb
+
+%% Request for graceful shutdown at remove_transport, stop_service of
+%% application shutdown.
+transition({shutdown = T, Pid}, S) ->
+ transition({T, Pid, transport}, S);
+transition({shutdown, Pid, Reason}, #state{parent = Pid, dpr = false} = S) ->
+ dpr(Reason, S);
+transition({shutdown, Pid, _}, #state{parent = Pid}) ->
ok;
-%% Request to close the transport connection.
-transition({close = T, Pid}, #state{parent = Pid,
- transport = TPid}) ->
- diameter_peer:close(TPid),
- {stop, T};
-
%% DPA reception has timed out.
transition(dpa_timeout, _) ->
stop;
@@ -425,6 +444,11 @@ transition({state, Pid}, #state{state = S, transport = TPid}) ->
%% Crash on anything unexpected.
+capx(recv_CER) ->
+ 'CER';
+capx({'Wait-CEA', _, _}) ->
+ 'CEA'.
+
%% start_next/1
start_next(#state{service = Svc0} = S) ->
@@ -440,18 +464,23 @@ start_next(#state{service = Svc0} = S) ->
%% send_CER/1
-send_CER(#state{mode = {connect, Remote},
- service = #diameter_service{capabilities = Caps},
+send_CER(#state{state = {'Wait-Conn-Ack', Tmo},
+ mode = {connect, Remote},
+ service = #diameter_service{capabilities = LCaps},
transport = TPid}
= S) ->
- OH = Caps#diameter_caps.origin_host,
+ OH = LCaps#diameter_caps.origin_host,
req_send_CER(OH, Remote)
orelse
- close({already_connected, Remote, Caps}, S),
+ close({already_connected, Remote, LCaps}, S),
CER = build_CER(S),
?LOG(send, 'CER'),
- send(TPid, encode(CER)),
- start_timer(S#state{state = 'Wait-CEA'}).
+ #diameter_packet{header = #diameter_header{end_to_end_id = Eid,
+ hop_by_hop_id = Hid}}
+ = Pkt
+ = encode(CER),
+ send(TPid, Pkt),
+ start_timer(Tmo, S#state{state = {'Wait-CEA', Hid, Eid}}).
%% Register ourselves as connecting to the remote endpoint in
%% question. This isn't strictly necessary since a peer implementing
@@ -463,16 +492,16 @@ send_CER(#state{mode = {connect, Remote},
req_send_CER(OriginHost, Remote) ->
register_everywhere({?MODULE, connection, OriginHost, {remote, Remote}}).
-%% start_timer/1
+%% start_timer/2
-start_timer(#state{state = PS} = S) ->
- erlang:send_after(?EVENT_TIMEOUT, self(), {timeout, PS}),
+start_timer(Tmo, #state{state = PS} = S) ->
+ erlang:send_after(Tmo, self(), {timeout, PS}),
S.
%% build_CER/1
-build_CER(#state{service = #diameter_service{capabilities = Caps}}) ->
- {ok, CER} = diameter_capx:build_CER(Caps),
+build_CER(#state{service = #diameter_service{capabilities = LCaps}}) ->
+ {ok, CER} = diameter_capx:build_CER(LCaps),
CER.
%% encode/1
@@ -482,10 +511,8 @@ encode(Rec) ->
Hdr = #diameter_header{version = ?DIAMETER_VERSION,
end_to_end_id = Seq,
hop_by_hop_id = Seq},
- Pkt = #diameter_packet{header = Hdr,
- msg = Rec},
- #diameter_packet{bin = Bin} = diameter_codec:encode(?BASE, Pkt),
- Bin.
+ diameter_codec:encode(?BASE, #diameter_packet{header = Hdr,
+ msg = Rec}).
sequence() ->
case getr(?SEQUENCE_KEY) of
@@ -553,7 +580,14 @@ discard(Reason, F, A) ->
%% rcv/3
%% Incoming CEA.
-rcv('CEA', Pkt, #state{state = 'Wait-CEA'} = S) ->
+rcv('CEA',
+ #diameter_packet{header = #diameter_header{end_to_end_id = Eid,
+ hop_by_hop_id = Hid}}
+ = Pkt,
+ #state{state = {'Wait-CEA' = T, Hid, Eid}}
+ = S) ->
+ handle_CEA(Pkt, S#state{state = T});
+rcv('CEA', Pkt, #state{state = 'Wait-CEA'} = S) -> %% old code
handle_CEA(Pkt, S);
%% Incoming CER
@@ -573,16 +607,16 @@ rcv(N, Pkt, S)
N == 'DPR' ->
handle_request(N, Pkt, S);
-%% DPA even though we haven't sent DPR: ignore.
-rcv('DPA', _Pkt, #state{dpr = false}) ->
- ok;
-
-%% DPA in response to DPR. We could check the sequence numbers but
-%% don't bother, just close.
-rcv('DPA' = N, _Pkt, #state{transport = TPid}) ->
+%% DPA in response to DPR and with the expected identifiers.
+rcv('DPA' = N,
+ #diameter_packet{header = #diameter_header{end_to_end_id = Eid,
+ hop_by_hop_id = Hid}},
+ #state{transport = TPid,
+ dpr = {Hid, Eid}}) ->
diameter_peer:close(TPid),
{stop, N};
+%% Ignore anything else, an unsolicited DPA in particular.
rcv(_, _, _) ->
ok.
@@ -800,8 +834,8 @@ a('CER', #diameter_caps{vendor_id = Vid,
{'Product-Name', Name},
{'Origin-State-Id', OSI}];
-a('DPR', #diameter_caps{origin_host = Host,
- origin_realm = Realm}) ->
+a('DPR', #diameter_caps{origin_host = {Host, _},
+ origin_realm = {Realm, _}}) ->
['DPA', {'Origin-Host', Host},
{'Origin-Realm', Realm}].
@@ -909,7 +943,9 @@ rejected(N)
%% open/5
-open(Pkt, SupportedApps, Caps, {Type, IS}, #state{parent = Pid} = S) ->
+open(Pkt, SupportedApps, Caps, {Type, IS}, #state{parent = Pid,
+ service = Svc}
+ = S) ->
#diameter_caps{origin_host = {_,_} = H,
inband_security_id = {LS,_}}
= Caps,
@@ -917,7 +953,9 @@ open(Pkt, SupportedApps, Caps, {Type, IS}, #state{parent = Pid} = S) ->
tls_ack(lists:member(?TLS, LS), Caps, Type, IS, S),
Pid ! {open, self(), H, {Caps, SupportedApps, Pkt}},
- S#state{state = 'Open'}.
+ %% Replace capabilities record with local/remote pairs.
+ S#state{state = 'Open',
+ service = Svc#diameter_service{capabilities = Caps}}.
%% We've advertised TLS support: tell the transport the result
%% and expect a reply when the handshake is complete.
@@ -970,24 +1008,113 @@ dwa(#diameter_caps{origin_host = OH,
{'Origin-State-Id', OSI}].
%% dpr/2
+%%
+%% The RFC isn't clear on whether DPR should be send in a non-Open
+%% state. The Peer State Machine transitions it documents aren't
+%% exhaustive (no Stop in Wait-I-CEA for example) so assume it's up to
+%% the implementation and transition to Closed (ie. die) if we haven't
+%% yet reached Open.
+
+%% Connection is open, DPR has not been sent.
+dpr(Reason, #state{state = 'Open',
+ dpr = false,
+ service = #diameter_service{capabilities = Caps}}
+ = S) ->
+ case getr(?DPR_KEY) of
+ CBs when is_list(CBs) ->
+ Ref = getr(?REF_KEY),
+ Peer = {self(), Caps},
+ dpr(CBs, [Reason, Ref, Peer], S);
+ undefined -> %% started in old code
+ send_dpr(Reason, [], S)
+ end;
-dpr(Cause, #state{transport = TPid,
- service = #diameter_service{capabilities = Caps}}
- = S) ->
- #diameter_caps{origin_host = OH,
- origin_realm = OR}
+%% Connection is open, DPR already sent.
+dpr(_, #state{state = 'Open'}) ->
+ ok;
+
+%% Connection not open.
+dpr(_Reason, _S) ->
+ stop.
+
+%% dpr/3
+%%
+%% Note that an implementation that wants to do something
+%% transport_module-specific can lookup the pid of the transport
+%% process and contact it. (eg. diameter:service_info/2)
+
+dpr([CB|Rest], [Reason | _] = Args, S) ->
+ try diameter_lib:eval([CB | Args]) of
+ {dpr, Opts} when is_list(Opts) ->
+ send_dpr(Reason, Opts, S);
+ dpr ->
+ send_dpr(Reason, [], S);
+ close = T ->
+ {stop, {disconnect_cb, T}};
+ ignore ->
+ dpr(Rest, Args, S);
+ T ->
+ No = {disconnect_cb, T},
+ diameter_lib:error_report(invalid, No),
+ {stop, No}
+ catch
+ E:R ->
+ No = {disconnect_cb, E, R, ?STACK},
+ diameter_lib:error_report(failure, No),
+ {stop, No}
+ end;
+
+dpr([], [Reason | _], S) ->
+ send_dpr(Reason, [], S).
+
+-record(opts, {cause, timeout = ?DPA_TIMEOUT}).
+
+send_dpr(Reason, Opts, #state{transport = TPid,
+ service = #diameter_service{capabilities = Caps}}
+ = S) ->
+ #opts{cause = Cause, timeout = Tmo}
+ = lists:foldl(fun opt/2,
+ #opts{cause = case Reason of
+ transport -> ?GOAWAY;
+ _ -> ?REBOOT
+ end,
+ timeout = ?DPA_TIMEOUT},
+ Opts),
+ #diameter_caps{origin_host = {OH, _},
+ origin_realm = {OR, _}}
= Caps,
- Bin = encode(['DPR', {'Origin-Host', OH},
+ #diameter_packet{header = #diameter_header{end_to_end_id = Eid,
+ hop_by_hop_id = Hid}}
+ = Pkt
+ = encode(['DPR', {'Origin-Host', OH},
{'Origin-Realm', OR},
{'Disconnect-Cause', Cause}]),
- send(TPid, Bin),
- dpa_timer(),
+ send(TPid, Pkt),
+ dpa_timer(Tmo),
?LOG(send, 'DPR'),
- S#state{dpr = diameter_codec:sequence_numbers(Bin)}.
-
-dpa_timer() ->
- erlang:send_after(?DPA_TIMEOUT, self(), dpa_timeout).
+ S#state{dpr = {Hid, Eid}}.
+
+opt({timeout, Tmo}, Rec)
+ when ?IS_TIMEOUT(Tmo) ->
+ Rec#opts{timeout = Tmo};
+opt({cause, Cause}, Rec)
+ when ?IS_CAUSE(Cause) ->
+ Rec#opts{cause = cause(Cause)};
+opt(T, _) ->
+ ?ERROR({invalid_option, T}).
+
+cause(rebooting) -> ?REBOOT;
+cause(goaway) -> ?GOAWAY;
+cause(busy) -> ?BUSY;
+cause(N)
+ when ?IS_CAUSE(N) ->
+ N;
+cause(N) ->
+ ?ERROR({invalid_cause, N}).
+
+dpa_timer(Tmo) ->
+ erlang:send_after(Tmo, self(), dpa_timeout).
%% register_everywhere/1
%%
diff --git a/lib/diameter/src/base/diameter_service.erl b/lib/diameter/src/base/diameter_service.erl
index cffba4fc94..29046e6462 100644
--- a/lib/diameter/src/base/diameter_service.erl
+++ b/lib/diameter/src/base/diameter_service.erl
@@ -494,7 +494,7 @@ handle_call({info, Item}, _From, S) ->
{reply, service_info(Item, S), S};
handle_call(stop, _From, S) ->
- shutdown(S),
+ shutdown(service, S),
{stop, normal, ok, S};
%% The server currently isn't guaranteed to be dead when the caller
%% gets the reply. We deal with this in the call to the server,
@@ -681,9 +681,10 @@ upgrade_insert(#state{service = #diameter_service{pid = Pid}} = S) ->
%%% ---------------------------------------------------------------------------
terminate(Reason, #state{service_name = Name} = S) ->
+ send_event(Name, stop),
ets:delete(?STATE_TABLE, Name),
shutdown == Reason %% application shutdown
- andalso shutdown(S).
+ andalso shutdown(application, S).
%%% ---------------------------------------------------------------------------
%%% # code_change(FromVsn, State, Extra)
@@ -766,44 +767,48 @@ mod_state(Alias, ModS) ->
%%% # shutdown/2
%%% ---------------------------------------------------------------------------
-shutdown(Refs, #state{peerT = PeerT}) ->
- ets:foldl(fun(P,ok) -> s(P, Refs), ok end, ok, PeerT).
+%% remove_transport: ask watchdogs to terminate their transport.
+shutdown(Refs, #state{peerT = PeerT})
+ when is_list(Refs) ->
+ ets:foldl(fun(P,ok) -> sp(P, Refs), ok end, ok, PeerT);
-s(#peer{ref = Ref, pid = Pid}, Refs) ->
- s(lists:member(Ref, Refs), Pid);
-
-s(true, Pid) ->
- Pid ! {shutdown, self()}; %% 'DOWN' will cleanup as usual
-s(false, _) ->
- ok.
-
-%%% ---------------------------------------------------------------------------
-%%% # shutdown/1
-%%% ---------------------------------------------------------------------------
-
-shutdown(#state{peerT = PeerT}) ->
+%% application/service shutdown: ask transports to terminate themselves.
+shutdown(Reason, #state{peerT = PeerT}) ->
%% A transport might not be alive to receive the shutdown request
%% but give those that are a chance to shutdown gracefully.
- wait(fun st/2, PeerT),
+ shutdown(conn, Reason, PeerT),
%% Kill the watchdogs explicitly in case there was no transport.
- wait(fun sw/2, PeerT).
+ shutdown(peer, Reason, PeerT).
-wait(Fun, T) ->
- diameter_lib:wait(ets:foldl(Fun, [], T)).
+%% sp/2
-st(#peer{op_state = {OS,_}} = P, Acc) ->
- st(P#peer{op_state = OS}, Acc);
-st(#peer{op_state = ?STATE_UP, conn = Pid}, Acc) ->
- Pid ! shutdown,
- [Pid | Acc];
-st(#peer{}, Acc) ->
- Acc.
+sp(#peer{ref = Ref, pid = Pid}, Refs) ->
+ lists:member(Ref, Refs)
+ andalso (Pid ! {shutdown, self()}). %% 'DOWN' cleans up
+
+%% shutdown/3
+
+shutdown(Who, Reason, T) ->
+ diameter_lib:wait(ets:foldl(fun(X,A) -> shutdown(Who, X, Reason, A) end,
+ [],
+ T)).
+
+shutdown(conn = Who, #peer{op_state = {OS,_}} = P, Reason, Acc) ->
+ shutdown(Who, P#peer{op_state = OS}, Reason, Acc);
-sw(#peer{pid = Pid}, Acc)
+shutdown(conn,
+ #peer{pid = Pid, op_state = ?STATE_UP, conn = TPid},
+ Reason,
+ Acc) ->
+ TPid ! {shutdown, Pid, Reason},
+ [TPid | Acc];
+
+shutdown(peer, #peer{pid = Pid}, _Reason, Acc)
when is_pid(Pid) ->
exit(Pid, shutdown),
[Pid | Acc];
-sw(#peer{}, Acc) ->
+
+shutdown(_, #peer{}, _, Acc) ->
Acc.
%%% ---------------------------------------------------------------------------
@@ -857,6 +862,7 @@ i(SvcName) ->
lists:foreach(fun(T) -> start_fsm(T,S) end, CL),
init_shared(S),
+ send_event(SvcName, start),
S.
cfg_acc({SvcName, #diameter_service{applications = Apps} = Rec, Opts},
@@ -2171,15 +2177,13 @@ reply([Msg], Dict, TPid, Fs, Pkt)
reply(Msg, Dict, TPid, Fs, Pkt#diameter_packet{errors = []});
%% No errors or a diameter_header/avp list.
-reply(Msg, Dict, TPid, Fs, #diameter_packet{errors = Es,
- transport_data = TD}
- = ReqPkt)
+reply(Msg, Dict, TPid, Fs, #diameter_packet{errors = Es} = ReqPkt)
when [] == Es;
is_record(hd(Msg), diameter_header) ->
Pkt = diameter_codec:encode(Dict, make_answer_packet(Msg, ReqPkt)),
eval_packet(Pkt, Fs),
incr(send, Pkt, Dict, TPid), %% count result codes in sent answers
- send(TPid, Pkt#diameter_packet{transport_data = TD});
+ send(TPid, Pkt);
%% Or not: set Result-Code and Failed-AVP AVP's.
reply(Msg, Dict, TPid, Fs, #diameter_packet{errors = [H|_] = Es} = Pkt) ->
@@ -2194,23 +2198,36 @@ eval_packet(Pkt, Fs) ->
%% make_answer_packet/2
+%% A reply message clears the R and T flags and retains the P flag.
+%% The E flag will be set at encode. 6.2 of 3588 requires the same P
+%% flag on an answer as on the request. A #diameter_packet{} returned
+%% from a handle_request callback can circumvent this by setting its
+%% own header values.
+make_answer_packet(#diameter_packet{header = Hdr,
+ msg = Msg,
+ transport_data = TD},
+ #diameter_packet{header = ReqHdr}) ->
+ Hdr0 = ReqHdr#diameter_header{version = ?DIAMETER_VERSION,
+ is_request = false,
+ is_error = undefined,
+ is_retransmitted = false},
+ #diameter_packet{header = fold_record(Hdr0, Hdr),
+ msg = Msg,
+ transport_data = TD};
+
%% Binaries and header/avp lists are sent as-is.
-make_answer_packet(Bin, _)
+make_answer_packet(Bin, #diameter_packet{transport_data = TD})
when is_binary(Bin) ->
- #diameter_packet{bin = Bin};
-make_answer_packet([#diameter_header{} | _] = Msg, _) ->
- #diameter_packet{msg = Msg};
-
-%% Otherwise a reply message clears the R and T flags and retains the
-%% P flag. The E flag will be set at encode. 6.2 of 3588 requires the
-%% same P flag on an answer as on the request.
-make_answer_packet(Msg, #diameter_packet{header = ReqHdr}) ->
- Hdr = ReqHdr#diameter_header{version = ?DIAMETER_VERSION,
- is_request = false,
- is_error = undefined,
- is_retransmitted = false},
- #diameter_packet{header = Hdr,
- msg = Msg}.
+ #diameter_packet{bin = Bin,
+ transport_data = TD};
+make_answer_packet([#diameter_header{} | _] = Msg,
+ #diameter_packet{transport_data = TD}) ->
+ #diameter_packet{msg = Msg,
+ transport_data = TD};
+
+%% Otherwise, preserve transport_data.
+make_answer_packet(Msg, #diameter_packet{transport_data = TD} = Pkt) ->
+ make_answer_packet(#diameter_packet{msg = Msg, transport_data = TD}, Pkt).
%% rc/1
diff --git a/lib/diameter/src/base/diameter_watchdog.erl b/lib/diameter/src/base/diameter_watchdog.erl
index d814f1afe2..243ad0a986 100644
--- a/lib/diameter/src/base/diameter_watchdog.erl
+++ b/lib/diameter/src/base/diameter_watchdog.erl
@@ -48,18 +48,19 @@
-record(watchdog,
{%% PCB - Peer Control Block; see RFC 3539, Appendix A
status = initial :: initial | okay | suspect | down | reopen,
- pending = false :: boolean(),
+ pending = false :: boolean(), %% DWA
tw :: 6000..16#FFFFFFFF | {module(), atom(), list()},
%% {M,F,A} -> integer() >= 0
num_dwa = 0 :: -1 | non_neg_integer(),
%% number of DWAs received during reopen
%% end PCB
- parent = self() :: pid(),
- transport :: pid() | undefined,
+ parent = self() :: pid(), %% service process
+ transport :: pid() | undefined, %% peer_fsm process
tref :: reference(), %% reference for current watchdog timer
message_data, %% term passed into diameter_service with message
sequence :: diameter:sequence(), %% mask
- restrict :: {diameter:restriction(), boolean()}}).
+ restrict :: {diameter:restriction(), boolean()},
+ shutdown = false :: boolean()}).
%% start/2
%%
@@ -168,7 +169,8 @@ handle_info(T, S) ->
handle_info(T, upgrade(S)).
upgrade(S) ->
- #watchdog{} = list_to_tuple(tuple_to_list(S) ++ [?NOMASK, {nodes, true}]).
+ #watchdog{} = list_to_tuple(tuple_to_list(S)
+ ++ [?NOMASK, {nodes, true}, false]).
event(#watchdog{status = T}, #watchdog{status = T}) ->
ok;
@@ -225,9 +227,10 @@ transition({shutdown, Pid}, #watchdog{parent = Pid,
down = S, %% sanity check
stop;
transition({shutdown = T, Pid}, #watchdog{parent = Pid,
- transport = TPid}) ->
+ transport = TPid}
+ = S) ->
TPid ! {T, self()},
- ok;
+ S#watchdog{shutdown = true};
%% Parent process has died,
transition({'DOWN', _, process, Pid, _Reason},
@@ -301,7 +304,10 @@ transition({open = P, TPid, _Hosts, T},
transition({'DOWN', _, process, TPid, _},
#watchdog{transport = TPid,
- status = initial}) ->
+ status = S,
+ shutdown = D})
+ when S == initial;
+ D ->
stop;
transition({'DOWN', _, process, TPid, _},
@@ -481,6 +487,14 @@ throwaway(S) ->
throw({?MODULE, throwaway, S}).
%% rcv/2
+%%
+%% The lack of Hop-by-Hop and End-to-End Identifiers checks in a
+%% received DWA is intentional. The purpose of the message is to
+%% demonstrate life but a peer that consistently bungles it by sending
+%% the wrong identifiers causes the connection to toggle between OPEN
+%% and SUSPECT, with failover and failback as result, despite there
+%% being no real problem with connectivity. Thus, relax and accept any
+%% incoming DWA as being in response to an outgoing DWR.
%% INITIAL Receive DWA Pending = FALSE
%% Throwaway() INITIAL
diff --git a/lib/diameter/test/diameter_dpr_SUITE.erl b/lib/diameter/test/diameter_dpr_SUITE.erl
new file mode 100644
index 0000000000..9252650bf7
--- /dev/null
+++ b/lib/diameter/test/diameter_dpr_SUITE.erl
@@ -0,0 +1,196 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2012. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%
+%% Tests of the disconnect_cb configuration.
+%%
+
+-module(diameter_dpr_SUITE).
+
+-export([suite/0,
+ all/0,
+ groups/0,
+ init_per_group/2,
+ end_per_group/2]).
+
+%% testcases
+-export([start/1,
+ connect/1,
+ remove_transport/1,
+ stop_service/1,
+ check/1,
+ stop/1]).
+
+%% disconnect_cb
+-export([disconnect/5]).
+
+-include("diameter.hrl").
+
+%% ===========================================================================
+
+-define(util, diameter_util).
+
+-define(ADDR, {127,0,0,1}).
+
+-define(CLIENT, "CLIENT").
+-define(SERVER, "SERVER").
+
+-define(DICT_COMMON, ?DIAMETER_DICT_COMMON).
+-define(APP_ID, ?DICT_COMMON:id()).
+
+%% Config for diameter:start_service/2.
+-define(SERVICE(Host),
+ [{'Origin-Host', Host},
+ {'Origin-Realm', "erlang.org"},
+ {'Host-IP-Address', [?ADDR]},
+ {'Vendor-Id', hd(Host)}, %% match this in disconnect/5
+ {'Product-Name', "OTP/diameter"},
+ {'Acct-Application-Id', [?APP_ID]},
+ {restrict_connections, false},
+ {application, [{dictionary, ?DICT_COMMON},
+ {module, #diameter_callback{_ = false}}]}]).
+
+%% Disconnect reasons that diameter passes as the first argument of a
+%% function configured as disconnect_cb.
+-define(REASONS, [transport, service, application]).
+
+%% Valid values for Disconnect-Cause.
+-define(CAUSES, [0, rebooting, 1, busy, 2, goaway]).
+
+%% Establish one client connection for element of this list,
+%% configured with disconnect/5 as disconnect_cb and returning the
+%% specified value.
+-define(RETURNS,
+ [[close, {dpr, [{cause, invalid}]}], [ignore, close], []]
+ ++ [[{dpr, [{timeout, 5000}, {cause, T}]}] || T <- ?CAUSES]).
+
+%% ===========================================================================
+
+suite() ->
+ [{timetrap, {seconds, 60}}].
+
+all() ->
+ [{group, R} || R <- ?REASONS].
+
+%% The group determines how transports are terminated: by remove_transport,
+%% stop_service or application stop.
+groups() ->
+ Ts = tc(),
+ [{R, [], Ts} || R <- ?REASONS].
+
+init_per_group(Name, Config) ->
+ [{group, Name} | Config].
+
+end_per_group(_, _) ->
+ ok.
+
+tc() ->
+ [start, connect, remove_transport, stop_service, check, stop].
+
+%% ===========================================================================
+%% start/stop testcases
+
+start(_Config) ->
+ ok = diameter:start(),
+ ok = diameter:start_service(?SERVER, ?SERVICE(?SERVER)),
+ ok = diameter:start_service(?CLIENT, ?SERVICE(?CLIENT)).
+
+connect(Config) ->
+ Pid = spawn(fun init/0), %% process for disconnect_cb to bang
+ Grp = group(Config),
+ LRef = ?util:listen(?SERVER, tcp),
+ Refs = [?util:connect(?CLIENT, tcp, LRef, opts(RCs, {Grp, Pid}))
+ || RCs <- ?RETURNS],
+ ?util:write_priv(Config, config, [Pid | Refs]).
+
+%% Remove all the client transports only in the transport group.
+remove_transport(Config) ->
+ transport == group(Config)
+ andalso (ok = diameter:remove_transport(?CLIENT, true)).
+
+%% Stop the service only in the service group.
+stop_service(Config) ->
+ service == group(Config)
+ andalso (ok = diameter:stop_service(?CLIENT)).
+
+%% Check for callbacks and stop the service. (Not the other way around
+%% for the timing reason explained below.)
+check(Config) ->
+ Grp = group(Config),
+ [Pid | Refs] = ?util:read_priv(Config, config),
+ Pid ! self(), %% ask for dictionary
+ Dict = receive {Pid, D} -> D end, %% get it
+ check(Refs, ?RETURNS, Grp, Dict). %% check for callbacks
+
+stop(_Config) ->
+ ok = diameter:stop().
+
+%% Whether or not there are callbacks after diameter:stop() depends on
+%% timing as long as the server runs on the same node: a server
+%% transport could close the connection before the client has chance
+%% to apply its callback. Therefore, just check that there haven't
+%% been any callbacks yet.
+check(_, _, application, Dict) ->
+ [] = dict:to_list(Dict);
+
+check([], [], _, _) ->
+ ok;
+
+check([Ref | Refs], CBs, Grp, Dict) ->
+ check1(Ref, hd(CBs), Grp, Dict),
+ check(Refs, tl(CBs), Grp, Dict).
+
+check1(Ref, [ignore | RCs], Reason, Dict) ->
+ check1(Ref, RCs, Reason, Dict);
+
+check1(Ref, [_|_], Reason, Dict) ->
+ {ok, Reason} = dict:find(Ref, Dict); %% callback with expected reason
+
+check1(Ref, [], _, Dict) ->
+ error = dict:find(Ref, Dict). %% no callback
+
+%% ----------------------------------------
+
+group(Config) ->
+ {group, Grp} = lists:keyfind(group, 1, Config),
+ Grp.
+
+%% Configure the callback with the group name (= disconnect reason) as
+%% extra argument.
+opts(RCs, T) ->
+ [{disconnect_cb, {?MODULE, disconnect, [T, RC]}} || RC <- RCs].
+
+%% Match the group name with the disconnect reason to ensure the
+%% callback is being called as expected.
+disconnect(Reason, Ref, Peer, {Reason, Pid}, RC) ->
+ io:format("disconnect: ~p ~p~n", [Ref, Reason]),
+ {_, #diameter_caps{vendor_id = {$C,$S}}} = Peer,
+ Pid ! {Reason, Ref},
+ RC.
+
+init() ->
+ exit(recv(dict:new())).
+
+recv(Dict) ->
+ receive
+ Pid when is_pid(Pid) ->
+ Pid ! {self(), Dict};
+ {Reason, Ref} ->
+ recv(dict:store(Ref, Reason, Dict))
+ end.
diff --git a/lib/diameter/test/diameter_traffic_SUITE.erl b/lib/diameter/test/diameter_traffic_SUITE.erl
index 5744ff0307..fa9333a226 100644
--- a/lib/diameter/test/diameter_traffic_SUITE.erl
+++ b/lib/diameter/test/diameter_traffic_SUITE.erl
@@ -38,6 +38,7 @@
result_codes/1,
send_ok/1,
send_nok/1,
+ send_bad_answer/1,
send_arbitrary/1,
send_unknown/1,
send_unknown_mandatory/1,
@@ -208,6 +209,7 @@ end_per_testcase(_, _) ->
tc() ->
[send_ok,
send_nok,
+ send_bad_answer,
send_arbitrary,
send_unknown,
send_unknown_mandatory,
@@ -308,6 +310,14 @@ send_nok(Config) ->
#'diameter_base_answer-message'{'Result-Code' = ?INVALID_AVP_BITS}
= call(Config, Req).
+%% Send an accounting ACR that the server tries to answer with an
+%% inappropriate header, resulting in no answer being sent and the
+%% request timing out.
+send_bad_answer(Config) ->
+ Req = ['ACR', {'Accounting-Record-Type', ?EVENT_RECORD},
+ {'Accounting-Record-Number', 2}],
+ {error, timeout} = call(Config, Req).
+
%% Send an ASR with an arbitrary AVP and expect success and the same
%% AVP in the reply.
send_arbitrary(Config) ->
@@ -770,6 +780,21 @@ request(#diameter_base_accounting_ACR{'Accounting-Record-Number' = 0},
request(#diameter_base_accounting_ACR{'Session-Id' = SId,
'Accounting-Record-Type' = RT,
+ 'Accounting-Record-Number' = 2 = RN},
+ #diameter_caps{origin_host = {OH, _},
+ origin_realm = {OR, _}}) ->
+ Ans = ['ACA', {'Result-Code', ?SUCCESS},
+ {'Session-Id', SId},
+ {'Origin-Host', OH},
+ {'Origin-Realm', OR},
+ {'Accounting-Record-Type', RT},
+ {'Accounting-Record-Number', RN}],
+
+ {reply, #diameter_packet{header = #diameter_header{is_error = true},%% not
+ msg = Ans}};
+
+request(#diameter_base_accounting_ACR{'Session-Id' = SId,
+ 'Accounting-Record-Type' = RT,
'Accounting-Record-Number' = RN},
#diameter_caps{origin_host = {OH, _},
origin_realm = {OR, _}}) ->
diff --git a/lib/diameter/test/modules.mk b/lib/diameter/test/modules.mk
index 7f163536fb..5898e125ae 100644
--- a/lib/diameter/test/modules.mk
+++ b/lib/diameter/test/modules.mk
@@ -2,7 +2,7 @@
# %CopyrightBegin%
#
-# Copyright Ericsson AB 2010-2011. All Rights Reserved.
+# Copyright Ericsson AB 2010-2012. All Rights Reserved.
#
# The contents of this file are subject to the Erlang Public License,
# Version 1.1, (the "License"); you may not use this file except in
@@ -39,7 +39,8 @@ MODULES = \
diameter_traffic_SUITE \
diameter_relay_SUITE \
diameter_tls_SUITE \
- diameter_failover_SUITE
+ diameter_failover_SUITE \
+ diameter_dpr_SUITE
HRL_FILES = \
diameter_ct.hrl