@@ -9,11 +9,19 @@ defmodule SseUser do
9
9
:current_message ,
10
10
:url ,
11
11
:sse_timeout ,
12
- :start_publisher_callback
12
+ :start_publisher_callback ,
13
+ :connect_callback ,
14
+ :conn_pid ,
15
+ :stream_ref ,
16
+ :last_event_id ,
17
+ :reconnect
13
18
]
14
19
end
15
20
16
- defp build_headers ( context , topic ) do
21
+ defp last_event_id_header ( nil ) , do: [ ]
22
+ defp last_event_id_header ( last_event_id ) , do: [ { "Last-Event-ID" , last_event_id } ]
23
+
24
+ defp build_headers ( context , state , topic ) do
17
25
iat = :os . system_time ( :second )
18
26
exp = iat + context . sse_jwt_expiration
19
27
@@ -35,7 +43,7 @@ defmodule SseUser do
35
43
[
36
44
{ [ "Authorization" ] , "Bearer #{ compact_signed } " } ,
37
45
{ [ "User-Agent" ] , context . sse_user_agent }
38
- ]
46
+ ] ++ last_event_id_header ( state . last_event_id )
39
47
end
40
48
41
49
def run ( context , user_name , topic , expected_messages ) do
@@ -45,8 +53,6 @@ defmodule SseUser do
45
53
"#{ user_name } : Starting SSE client on url #{ url } , topic #{ topic } , expecting #{ length ( expected_messages ) } messages"
46
54
end )
47
55
48
- headers = build_headers ( context , topic )
49
-
50
56
parsed_url = URI . parse ( url )
51
57
52
58
opts = % {
@@ -59,10 +65,18 @@ defmodule SseUser do
59
65
}
60
66
}
61
67
62
- { :ok , conn_pid } = :gun . open ( String . to_atom ( parsed_url . host ) , parsed_url . port , opts )
63
- { :ok , proto } = :gun . await_up ( conn_pid )
64
- Logger . debug ( fn -> "Connection established with proto #{ inspect ( proto ) } " end )
65
- stream_ref = :gun . get ( conn_pid , parsed_url . path , headers )
68
+ connect_callback = fn state ->
69
+ headers = build_headers ( context , state , topic )
70
+ { :ok , conn_pid } = :gun . open ( String . to_atom ( parsed_url . host ) , parsed_url . port , opts )
71
+ { :ok , proto } = :gun . await_up ( conn_pid )
72
+ Logger . debug ( fn -> "Connection established with proto #{ inspect ( proto ) } " end )
73
+ stream_ref = :gun . get ( conn_pid , parsed_url . path , headers )
74
+ state = Map . put ( state , :conn_pid , conn_pid )
75
+ state = Map . put ( state , :stream_ref , stream_ref )
76
+ state
77
+ end
78
+
79
+ reconnect = if context . sse_auto_reconnect , do: 0 , else: - 1
66
80
67
81
state = % SseState {
68
82
user_name: user_name ,
@@ -73,71 +87,108 @@ defmodule SseUser do
73
87
sse_timeout: context . sse_timeout ,
74
88
start_publisher_callback: fn ->
75
89
LoadTest.Main . start_publisher ( context , user_name , topic , expected_messages )
76
- end
90
+ end ,
91
+ connect_callback: connect_callback ,
92
+ last_event_id: nil ,
93
+ reconnect: reconnect
77
94
}
78
95
79
- wait_for_messages ( state , conn_pid , stream_ref , expected_messages )
96
+ state = connect_callback . ( state )
97
+
98
+ wait_for_messages ( state , expected_messages )
80
99
end
81
100
82
- defp wait_for_messages ( state , conn_pid , stream_ref , [ first_message | remaining_messages ] ) do
101
+ defp reconnect ( state , messages , reason ) do
102
+ :ok = :gun . close ( state . conn_pid )
103
+
104
+ if state . reconnect == - 1 do
105
+ Logger . error ( fn -> "#{ header ( state ) } Connection closed: #{ reason } " end )
106
+ Stats . inc_msg_received_http_error ( )
107
+ raise ( "#{ header ( state ) } Connection closed" )
108
+ end
109
+
110
+ Logger . error ( "#{ header ( state ) } Connection closed, reconnecting: #{ reason } " )
111
+ Stats . inc_reconnect ( )
112
+ state = state . connect_callback . ( state )
113
+ state = Map . put ( state , :reconnect , state . reconnect + 1 )
114
+ wait_for_messages ( state , messages )
115
+ end
116
+
117
+ defp wait_for_messages ( state , [ first_message | remaining_messages ] ) do
83
118
Logger . debug ( fn -> "#{ header ( state ) } Waiting for message: #{ first_message } " end )
84
119
85
- result = :gun . await ( conn_pid , stream_ref , state . sse_timeout )
120
+ result = :gun . await ( state . conn_pid , state . stream_ref , state . sse_timeout )
86
121
87
122
case result do
88
123
{ :response , _ , code , _ } when code == 200 ->
89
124
Logger . debug (
90
125
"#{ header ( state ) } Connected, waiting: #{ length ( remaining_messages ) + 1 } messages, url #{ state . url } "
91
126
)
92
127
93
- state . start_publisher_callback . ( )
94
- wait_for_messages ( state , conn_pid , stream_ref , [ first_message | remaining_messages ] )
128
+ state =
129
+ if state . start_publisher_callback do
130
+ state . start_publisher_callback . ( )
131
+ Map . put ( state , :start_publisher_callback , nil )
132
+ else
133
+ state
134
+ end
135
+
136
+ wait_for_messages ( state , [ first_message | remaining_messages ] )
95
137
96
138
{ :response , _ , code , _ } ->
97
139
Logger . error ( "#{ header ( state ) } Error: #{ inspect ( code ) } " )
98
- :ok = :gun . close ( conn_pid )
140
+ :ok = :gun . close ( state . conn_pid )
99
141
Stats . inc_msg_received_http_error ( )
100
142
raise ( "#{ header ( state ) } Error" )
101
143
102
- { :data , _ , msg } ->
144
+ { :data , :fin , _ } ->
145
+ reconnect ( state , [ first_message | remaining_messages ] , "{:data, :fin, _}" )
146
+
147
+ { :error , { :stream_error , { :stream_error , :internal_error , :"Stream reset by server." } } } ->
148
+ reconnect ( state , [ first_message | remaining_messages ] , "Stream reset by server." )
149
+
150
+ { :data , :nofin , msg } ->
103
151
msg = String . trim ( msg )
104
152
Logger . debug ( fn -> "#{ header ( state ) } Received message: #{ inspect ( msg ) } " end )
105
153
106
154
if msg =~ "event: ping" do
107
- wait_for_messages ( state , conn_pid , stream_ref , [ first_message | remaining_messages ] )
155
+ wait_for_messages ( state , [ first_message | remaining_messages ] )
108
156
else
109
- if check_message ( state , msg , first_message ) == :error do
110
- :ok = :gun . close ( conn_pid )
111
- raise ( "#{ header ( state ) } Message check error" )
157
+ case check_message ( state , msg , first_message ) do
158
+ :error ->
159
+ :ok = :gun . close ( state . conn_pid )
160
+ raise ( "#{ header ( state ) } Message check error" )
161
+
162
+ state ->
163
+ state = Map . put ( state , :current_message , state . current_message + 1 )
164
+ wait_for_messages ( state , remaining_messages )
112
165
end
113
-
114
- state = Map . put ( state , :current_message , state . current_message + 1 )
115
- wait_for_messages ( state , conn_pid , stream_ref , remaining_messages )
116
166
end
117
167
118
168
msg ->
119
169
Logger . error ( "#{ header ( state ) } Unexpected message #{ inspect ( msg ) } " )
120
- :ok = :gun . close ( conn_pid )
170
+ :ok = :gun . close ( state . conn_pid )
121
171
raise ( "#{ header ( state ) } Unexpected message" )
122
172
end
123
173
end
124
174
125
- defp wait_for_messages ( state , conn_pid , _ , [ ] ) do
126
- :ok = :gun . close ( conn_pid )
127
- Logger . info ( "#{ header ( state ) } All messages received, url #{ state . url } " )
175
+ defp wait_for_messages ( state , [ ] ) do
176
+ :ok = :gun . close ( state . conn_pid )
177
+ Logger . info ( "#{ header ( state ) } All messages received" )
128
178
end
129
179
130
180
defp header ( state ) do
131
181
now = :os . system_time ( :millisecond )
132
182
133
- "#{ state . user_name } / #{ now - state . start_time } ms / #{ state . current_message } < #{ state . all_messages } : "
183
+ "#{ state . user_name } / #{ now - state . start_time } ms / #{ state . current_message } < #{ state . all_messages } / #{ state . reconnect } : "
134
184
end
135
185
136
186
defp check_message ( state , received_message , expected_message ) do
137
- clean_received_message = String . replace ( received_message , ~r" id: .*\n event: .*\n " , "" )
187
+ [ first , _ , third | _ ] = String . split ( received_message , "\n " )
188
+ [ _ , id ] = String . split ( first , " " )
138
189
139
190
try do
140
- [ _ , ts , message , _ , _ ] = String . split ( clean_received_message , " " , parts: 5 )
191
+ [ _ , ts , message , _ , _ ] = String . split ( third , " " , parts: 5 )
141
192
current_ts = :os . system_time ( :millisecond )
142
193
delay = current_ts - String . to_integer ( ts )
143
194
Stats . observe_propagation ( delay )
@@ -148,7 +199,8 @@ defmodule SseUser do
148
199
149
200
if message == expected_message do
150
201
Stats . inc_msg_received_ok ( )
151
- :ok
202
+ state = Map . put ( state , :last_event_id , id )
203
+ state
152
204
else
153
205
Stats . inc_msg_received_unexpected_message ( )
154
206
0 commit comments