lowdown 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +121 -0
- data/Gemfile +11 -3
- data/README.md +145 -14
- data/Rakefile +13 -6
- data/bin/lowdown +38 -19
- data/examples/long-running.rb +63 -0
- data/examples/simple.rb +37 -0
- data/lib/lowdown.rb +2 -21
- data/lib/lowdown/certificate.rb +21 -1
- data/lib/lowdown/client.rb +156 -60
- data/lib/lowdown/client/request_group.rb +70 -0
- data/lib/lowdown/connection.rb +257 -182
- data/lib/lowdown/connection/monitor.rb +84 -0
- data/lib/lowdown/mock.rb +57 -49
- data/lib/lowdown/notification.rb +24 -6
- data/lib/lowdown/response.rb +9 -20
- data/lib/lowdown/version.rb +4 -1
- data/lowdown.gemspec +5 -3
- metadata +22 -4
- data/lib/lowdown/threading.rb +0 -188
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "celluloid/current"
|
4
|
+
|
5
|
+
module Lowdown
|
6
|
+
class Client
|
7
|
+
# Implements the {Connection::DelegateProtocol} to provide a way to group requests and hal the caller thread while
|
8
|
+
# waiting for the responses to come in. In addition to the regular delegate message based callbacks, it also allows
|
9
|
+
# for a more traditional blocks-based callback mechanism.
|
10
|
+
#
|
11
|
+
# @note These callbacks are executed on a separate thread, so be aware about this when accessing shared resources
|
12
|
+
# from a block callback.
|
13
|
+
#
|
14
|
+
class RequestGroup
|
15
|
+
attr_reader :callbacks
|
16
|
+
|
17
|
+
def initialize(client, condition)
|
18
|
+
@client = client
|
19
|
+
@callbacks = Callbacks.new(condition)
|
20
|
+
end
|
21
|
+
|
22
|
+
def send_notification(notification, delegate: nil, context: nil, &block)
|
23
|
+
return unless @callbacks.alive?
|
24
|
+
if (block.nil? && delegate.nil?) || (block && delegate)
|
25
|
+
raise ArgumentError, "Either a delegate object or a block should be provided."
|
26
|
+
end
|
27
|
+
@callbacks.add(notification.formatted_id, block || delegate)
|
28
|
+
@client.send_notification(notification, delegate: @callbacks.async, context: context)
|
29
|
+
end
|
30
|
+
|
31
|
+
def empty?
|
32
|
+
@callbacks.empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
def terminate
|
36
|
+
@callbacks.terminate if @callbacks.alive?
|
37
|
+
end
|
38
|
+
|
39
|
+
class Callbacks
|
40
|
+
include Celluloid
|
41
|
+
|
42
|
+
def initialize(condition)
|
43
|
+
@callbacks = {}
|
44
|
+
@condition = condition
|
45
|
+
end
|
46
|
+
|
47
|
+
def empty?
|
48
|
+
@callbacks.empty?
|
49
|
+
end
|
50
|
+
|
51
|
+
def add(notification_id, callback)
|
52
|
+
raise ArgumentError, "A notification ID is required." unless notification_id
|
53
|
+
@callbacks[notification_id] = callback
|
54
|
+
end
|
55
|
+
|
56
|
+
def handle_apns_response(response, context:)
|
57
|
+
callback = @callbacks.delete(response.id)
|
58
|
+
if callback.is_a?(Proc)
|
59
|
+
callback.call(response, context)
|
60
|
+
else
|
61
|
+
callback.send(:handle_apns_response, response, context: context)
|
62
|
+
end
|
63
|
+
ensure
|
64
|
+
@condition.signal if @callbacks.empty?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
data/lib/lowdown/connection.rb
CHANGED
@@ -1,44 +1,49 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# This file can’t specify that it uses frozen string literals yet, because the strings’ encodings are modified when
|
2
|
+
# passed to the http-2 gem.
|
3
3
|
|
4
|
-
require "
|
4
|
+
require "lowdown/response"
|
5
5
|
|
6
|
-
require "openssl"
|
7
|
-
require "socket"
|
8
|
-
require "timeout"
|
9
6
|
require "uri"
|
10
7
|
|
8
|
+
require "celluloid/current"
|
9
|
+
require "celluloid/io"
|
10
|
+
require "http/2"
|
11
|
+
|
11
12
|
if HTTP2::VERSION == "0.8.0"
|
12
13
|
# @!visibility private
|
13
14
|
#
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
15
|
+
module HTTP2
|
16
|
+
# @!visibility private
|
17
|
+
#
|
18
|
+
# This monkey-patch ensures that we send the HTTP/2 connection preface before anything else.
|
19
|
+
#
|
20
|
+
# @see https://github.com/igrigorik/http-2/pull/44
|
21
|
+
#
|
22
|
+
class Client
|
23
|
+
def connection_management(frame)
|
24
|
+
if @state == :waiting_connection_preface
|
25
|
+
send_connection_preface
|
26
|
+
connection_settings(frame)
|
27
|
+
else
|
28
|
+
super(frame)
|
29
|
+
end
|
25
30
|
end
|
26
31
|
end
|
27
|
-
end
|
28
32
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
33
|
+
# @!visibility private
|
34
|
+
#
|
35
|
+
# These monkey-patches ensure that data added to a buffer has a binary encoding, as to not lead to encoding clashes.
|
36
|
+
#
|
37
|
+
# @see https://github.com/igrigorik/http-2/pull/46
|
38
|
+
#
|
39
|
+
class Buffer
|
40
|
+
def <<(x)
|
41
|
+
super(x.force_encoding(Encoding::BINARY))
|
42
|
+
end
|
39
43
|
|
40
|
-
|
41
|
-
|
44
|
+
def prepend(x)
|
45
|
+
super(x.force_encoding(Encoding::BINARY))
|
46
|
+
end
|
42
47
|
end
|
43
48
|
end
|
44
49
|
end
|
@@ -49,14 +54,36 @@ module Lowdown
|
|
49
54
|
# It manages both the SSL connection and processing of the HTTP/2 data sent back and forth over that connection.
|
50
55
|
#
|
51
56
|
class Connection
|
57
|
+
class TimedOut < StandardError; end
|
58
|
+
|
59
|
+
CONNECT_RETRIES = 5
|
60
|
+
CONNECT_RETRY_BACKOFF = 5
|
61
|
+
CONNECT_TIMEOUT = 10
|
62
|
+
HEARTBEAT_INTERVAL = 10
|
63
|
+
HEARTBEAT_TIMEOUT = CONNECT_TIMEOUT
|
64
|
+
|
65
|
+
include Celluloid::IO
|
66
|
+
include Celluloid::Internals::Logger
|
67
|
+
finalizer :disconnect
|
68
|
+
|
52
69
|
# @param [URI, String] uri
|
53
70
|
# the details to connect to the APN service.
|
54
71
|
#
|
55
72
|
# @param [OpenSSL::SSL::SSLContext] ssl_context
|
56
73
|
# a SSL context, configured with the certificate/key pair, which is used to connect to the APN service.
|
57
74
|
#
|
58
|
-
|
75
|
+
# @param [Boolean] connect
|
76
|
+
# whether or not to immediately connect on initialization.
|
77
|
+
#
|
78
|
+
def initialize(uri, ssl_context, connect = true)
|
59
79
|
@uri, @ssl_context = URI(uri), ssl_context
|
80
|
+
reset_state!
|
81
|
+
|
82
|
+
if connect
|
83
|
+
# This ensures that calls to the public #connect method are ignored while already connecting.
|
84
|
+
@connecting = true
|
85
|
+
async.connect!
|
86
|
+
end
|
60
87
|
end
|
61
88
|
|
62
89
|
# @return [URI]
|
@@ -69,63 +96,188 @@ module Lowdown
|
|
69
96
|
#
|
70
97
|
attr_reader :ssl_context
|
71
98
|
|
72
|
-
# Creates a new SSL connection to the service, a HTTP/2 client, and starts off
|
99
|
+
# Creates a new SSL connection to the service, a HTTP/2 client, and starts off the main runloop.
|
73
100
|
#
|
74
101
|
# @return [void]
|
75
102
|
#
|
76
|
-
def
|
77
|
-
|
78
|
-
@requests = Threading::Counter.new
|
79
|
-
@worker = Worker.new(@uri, @ssl_context)
|
103
|
+
def connect
|
104
|
+
connect! unless @connecting
|
80
105
|
end
|
81
106
|
|
82
|
-
#
|
83
|
-
# pending jobs dispatched onto the main thread.
|
107
|
+
# Closes the connection and resets the internal state
|
84
108
|
#
|
85
109
|
# @return [void]
|
86
110
|
#
|
87
|
-
def
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
@worker = @requests = nil
|
111
|
+
def disconnect
|
112
|
+
@connection.close if @connection
|
113
|
+
@heartbeat.cancel if @heartbeat
|
114
|
+
reset_state!
|
92
115
|
end
|
93
116
|
|
94
|
-
#
|
117
|
+
# @return [Boolean]
|
118
|
+
# whether or not the Connection is open.
|
95
119
|
#
|
96
|
-
|
97
|
-
|
120
|
+
def connected?
|
121
|
+
!@connection.nil? && !@connection.closed?
|
122
|
+
end
|
123
|
+
|
124
|
+
# This performs a HTTP/2 PING to determine if the connection is actually alive. Be sure to not call this on a
|
125
|
+
# sleeping connection, or it will be guaranteed to fail.
|
126
|
+
#
|
127
|
+
# @note This halts the caller thread until a reply is received. You should call this on a future and possibly set
|
128
|
+
# a timeout.
|
98
129
|
#
|
99
130
|
# @return [Boolean]
|
100
|
-
# whether or not
|
131
|
+
# whether or not a reply was received.
|
101
132
|
#
|
102
|
-
def
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
133
|
+
def ping
|
134
|
+
if connected?
|
135
|
+
condition = Celluloid::Condition.new
|
136
|
+
@http.ping("whatever") { condition.signal(true) }
|
137
|
+
condition.wait
|
138
|
+
else
|
139
|
+
false
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def connect!(tries = 0)
|
146
|
+
return if @connection
|
147
|
+
@connecting = true
|
148
|
+
|
149
|
+
info "Opening APNS connection."
|
150
|
+
|
151
|
+
# Celluloid::IO::DNSResolver bug. In case there is no connection at all:
|
152
|
+
# 1. This results in `nil`:
|
153
|
+
# https://github.com/celluloid/celluloid-io/blob/85cee9da22ef5e94ba0abfd46454a2d56572aff4/lib/celluloid/io/dns_resolver.rb#L32
|
154
|
+
# 2. This tries to `NilClass#send` the hostname:
|
155
|
+
# https://github.com/celluloid/celluloid-io/blob/85cee9da22ef5e94ba0abfd46454a2d56572aff4/lib/celluloid/io/dns_resolver.rb#L44
|
156
|
+
begin
|
157
|
+
socket = TCPSocket.new(@uri.host, @uri.port)
|
158
|
+
rescue NoMethodError
|
159
|
+
raise SocketError, "(Probably) getaddrinfo: nodename nor servname provided, or not known"
|
160
|
+
end
|
161
|
+
|
162
|
+
@connection = SSLSocket.new(socket, @ssl_context)
|
163
|
+
begin
|
164
|
+
timeout(CONNECT_TIMEOUT) { @connection.connect }
|
165
|
+
rescue Celluloid::TimedOut
|
166
|
+
raise TimedOut, "Initiating SSL socket timed-out."
|
167
|
+
end
|
168
|
+
|
169
|
+
@http = HTTP2::Client.new
|
170
|
+
@http.on(:frame) do |bytes|
|
171
|
+
@connection.print(bytes)
|
172
|
+
@connection.flush
|
173
|
+
end
|
174
|
+
|
175
|
+
async.runloop
|
176
|
+
|
177
|
+
rescue Celluloid::TaskTerminated, Celluloid::DeadActorError
|
178
|
+
# These are legit, let them bubble up.
|
179
|
+
raise
|
180
|
+
rescue Exception => e
|
181
|
+
# The main reason to do connect retries ourselves, instead of letting it up a supervisor/pool, is because a pool
|
182
|
+
# goes into a bad state if a connection crashes on initialization.
|
183
|
+
@connection.close if @connection && !@connection.closed?
|
184
|
+
@connection = @http = nil
|
185
|
+
if tries < CONNECT_RETRIES
|
186
|
+
tries += 1
|
187
|
+
delay = tries * CONNECT_RETRY_BACKOFF
|
188
|
+
error("#{e.class}: #{e.message} - retrying in #{delay} seconds (#{tries}/#{CONNECT_RETRIES})")
|
189
|
+
after(delay) { async.connect!(tries) }
|
190
|
+
return
|
191
|
+
else
|
192
|
+
raise
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def reset_state!
|
197
|
+
@connecting = false
|
198
|
+
@connected = false
|
199
|
+
@request_queue = []
|
200
|
+
@connection = @http = @heartbeat = nil
|
201
|
+
end
|
202
|
+
|
203
|
+
# The main IO runloop that feeds data from the remote service into the HTTP/2 client.
|
204
|
+
#
|
205
|
+
# It should only ever exit gracefully if the connection has been closed with {#close} or the actor has been
|
206
|
+
# terminated. Otherwise this method may raise any connection or HTTP/2 parsing related exception, which will kill
|
207
|
+
# the actor and, if supervised, start a new connection.
|
208
|
+
#
|
209
|
+
# @return [void]
|
210
|
+
#
|
211
|
+
def runloop
|
212
|
+
loop do
|
213
|
+
begin
|
214
|
+
@http << @connection.readpartial(1024)
|
215
|
+
change_to_connected_state if !@connected && @http.state == :connected
|
216
|
+
rescue IOError => e
|
217
|
+
if @connection
|
218
|
+
raise
|
219
|
+
else
|
220
|
+
# Connection was closed by us and set to nil, so exit gracefully
|
221
|
+
break
|
222
|
+
end
|
108
223
|
end
|
109
|
-
Thread.stop
|
110
224
|
end
|
111
|
-
# If the thread was woken-up before the timeout was reached, that means we got a PONG.
|
112
|
-
true
|
113
|
-
rescue Timeout::Error
|
114
|
-
false
|
115
225
|
end
|
116
226
|
|
117
|
-
#
|
227
|
+
# Called when the HTTP client changes its state to `:connected`.
|
118
228
|
#
|
119
229
|
# @return [void]
|
120
230
|
#
|
121
|
-
def
|
122
|
-
|
123
|
-
|
231
|
+
def change_to_connected_state
|
232
|
+
@max_stream_count = @http.remote_settings[:settings_max_concurrent_streams]
|
233
|
+
@connected = true
|
234
|
+
|
235
|
+
debug "APNS connection established. Maximum number of concurrent streams: #{@max_stream_count}. " \
|
236
|
+
"Flushing #{@request_queue.size} enqueued requests."
|
237
|
+
|
238
|
+
@request_queue.size.times do
|
239
|
+
async.try_to_perform_request!
|
240
|
+
end
|
241
|
+
|
242
|
+
@heartbeat = every(HEARTBEAT_INTERVAL) do
|
243
|
+
debug "Sending heartbeat ping"
|
244
|
+
begin
|
245
|
+
future.ping.call(HEARTBEAT_TIMEOUT)
|
246
|
+
rescue Celluloid::TimedOut
|
247
|
+
raise TimedOut, "Heartbeat ping timed-out."
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
public
|
253
|
+
|
254
|
+
# This module describes the interface that your delegate object should conform to, but it is not required to include
|
255
|
+
# this module in your class, it mainly serves a documentation purpose.
|
256
|
+
#
|
257
|
+
module DelegateProtocol
|
258
|
+
# Called when a request is finished and a response is available.
|
259
|
+
#
|
260
|
+
# @note (see Connection#post)
|
261
|
+
#
|
262
|
+
# @param [Response] response
|
263
|
+
# the Response that holds the status data that came back from the service.
|
264
|
+
#
|
265
|
+
# @param [Object, nil] context
|
266
|
+
# the context passed in when making the request, which can be any type of object or an array of objects.
|
267
|
+
#
|
268
|
+
# @return [void]
|
269
|
+
#
|
270
|
+
def handle_apns_response(response, context:)
|
271
|
+
raise NotImplementedError
|
272
|
+
end
|
124
273
|
end
|
125
274
|
|
126
275
|
# Sends the provided data as a `POST` request to the service.
|
127
276
|
#
|
128
|
-
# @note
|
277
|
+
# @note It is strongly advised that the delegate object is a Celluloid actor and that you pass in an async proxy
|
278
|
+
# of that object, but that is not required. If you do not pass in an actor, then be advised that the
|
279
|
+
# callback will run on this connection’s private thread and thus you should not perform long blocking
|
280
|
+
# operations.
|
129
281
|
#
|
130
282
|
# @param [String] path
|
131
283
|
# the request path, which should be `/3/device/<device-token>`.
|
@@ -136,152 +288,75 @@ module Lowdown
|
|
136
288
|
# @param [String] body
|
137
289
|
# the (JSON) encoded payload data to send to the service.
|
138
290
|
#
|
139
|
-
# @
|
140
|
-
#
|
291
|
+
# @param [DelegateProtocol] delegate
|
292
|
+
# an object that implements the delegate protocol.
|
141
293
|
#
|
142
|
-
# @
|
143
|
-
#
|
294
|
+
# @param [Object, nil] context
|
295
|
+
# any object that you want to be passed to the delegate once the response is back.
|
144
296
|
#
|
145
297
|
# @return [void]
|
146
298
|
#
|
147
|
-
def post(path
|
148
|
-
request(
|
299
|
+
def post(path:, headers:, body:, delegate:, context: nil)
|
300
|
+
request("POST", path, headers, body, delegate, context)
|
149
301
|
end
|
150
302
|
|
151
303
|
private
|
152
304
|
|
153
|
-
|
154
|
-
@requests.increment!
|
155
|
-
@worker.enqueue do |http, callbacks|
|
156
|
-
headers = { ":method" => method.to_s, ":path" => path.to_s, "content-length" => body.bytesize.to_s }
|
157
|
-
custom_headers.each { |k, v| headers[k] = v.to_s }
|
158
|
-
|
159
|
-
stream = http.new_stream
|
160
|
-
response = Response.new
|
161
|
-
|
162
|
-
stream.on(:headers) do |response_headers|
|
163
|
-
response.headers = Hash[*response_headers.flatten]
|
164
|
-
end
|
305
|
+
Request = Struct.new(:headers, :body, :delegate, :context)
|
165
306
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
end
|
307
|
+
def request(method, path, custom_headers, body, delegate, context)
|
308
|
+
headers = { ":method" => method.to_s, ":path" => path.to_s, "content-length" => body.bytesize.to_s }
|
309
|
+
custom_headers.each { |k, v| headers[k] = v.to_s }
|
170
310
|
|
171
|
-
|
172
|
-
|
173
|
-
callback.call(response)
|
174
|
-
@requests.decrement!
|
175
|
-
end
|
176
|
-
end
|
311
|
+
request = Request.new(headers, body, delegate, context)
|
312
|
+
@request_queue << request
|
177
313
|
|
178
|
-
|
179
|
-
stream.data(body, end_stream: true)
|
180
|
-
end
|
314
|
+
try_to_perform_request!
|
181
315
|
end
|
182
316
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
# * HTTP2 client
|
188
|
-
# * Another thread from where request callbacks are ran
|
189
|
-
#
|
190
|
-
class Worker < Threading::Consumer
|
191
|
-
def initialize(uri, ssl_context)
|
192
|
-
@uri, @ssl_context = uri, ssl_context
|
193
|
-
|
194
|
-
# Start the worker thread.
|
195
|
-
#
|
196
|
-
# Because a max size of 0 is not allowed, create with an initial max size of 1 and add a dummy job. This is so
|
197
|
-
# that any attempt to add a new job to the queue is going to halt the calling thread *until* we change the max.
|
198
|
-
super(queue: Thread::SizedQueue.new(1))
|
199
|
-
# Put caller thread into sleep until connected.
|
200
|
-
Thread.stop
|
317
|
+
def try_to_perform_request!
|
318
|
+
unless @connected
|
319
|
+
debug "Defer performing request, because the connection has not been established yet"
|
320
|
+
return
|
201
321
|
end
|
202
322
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
#
|
207
|
-
def stop
|
208
|
-
thread[:should_exit] = true
|
209
|
-
thread.join
|
210
|
-
end
|
211
|
-
|
212
|
-
# @return [Boolean]
|
213
|
-
# whether or not the worker is still alive and kicking.
|
214
|
-
#
|
215
|
-
def working?
|
216
|
-
alive? && !empty? && !@callbacks.empty?
|
323
|
+
unless @http.active_stream_count < @max_stream_count
|
324
|
+
debug "Defer performing request, because the maximum concurren stream count has been reached"
|
325
|
+
return
|
217
326
|
end
|
218
327
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
@callbacks.kill
|
223
|
-
@ssl.close
|
224
|
-
super
|
328
|
+
unless request = @request_queue.shift
|
329
|
+
debug "Defer performing request, because the request queue is empty"
|
330
|
+
return
|
225
331
|
end
|
226
332
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
# Setup the request callbacks consumer here so its parent thread will be this worker thread.
|
231
|
-
@callbacks = Threading::Consumer.new
|
333
|
+
apns_id = request.headers["apns-id"]
|
334
|
+
debug "[#{apns_id}] Performing request"
|
232
335
|
|
233
|
-
|
234
|
-
|
235
|
-
@ssl.hostname = @uri.hostname
|
236
|
-
@ssl.connect
|
336
|
+
stream = @http.new_stream
|
337
|
+
response = Response.new
|
237
338
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
@ssl.print(bytes)
|
243
|
-
@ssl.flush
|
244
|
-
end
|
339
|
+
stream.on(:headers) do |headers|
|
340
|
+
headers = Hash[*headers.flatten]
|
341
|
+
debug "[#{apns_id}] Got response headers: #{headers.inspect}"
|
342
|
+
response.headers = headers
|
245
343
|
end
|
246
344
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
#
|
252
|
-
def change_to_connected_state
|
253
|
-
queue.max = @http.remote_settings[:settings_max_concurrent_streams]
|
254
|
-
@connected = true
|
255
|
-
parent_thread.run
|
345
|
+
stream.on(:data) do |data|
|
346
|
+
debug "[#{apns_id}] Got response data: #{data}"
|
347
|
+
response.raw_body ||= ""
|
348
|
+
response.raw_body << data
|
256
349
|
end
|
257
350
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
# whether or not the HTTP client’s state is `:connected`.
|
263
|
-
#
|
264
|
-
def http_connected?
|
265
|
-
@http.state == :connected
|
351
|
+
stream.on(:close) do
|
352
|
+
debug "[#{apns_id}] Request completed"
|
353
|
+
request.delegate.handle_apns_response(response, context: request.context)
|
354
|
+
async.try_to_perform_request!
|
266
355
|
end
|
267
356
|
|
268
|
-
|
269
|
-
|
270
|
-
until thread[:should_exit] || @ssl.closed?
|
271
|
-
# Once connected, add requests while the max stream count has not yet been reached.
|
272
|
-
if !@connected
|
273
|
-
change_to_connected_state if http_connected?
|
274
|
-
elsif @http.active_stream_count < queue.max
|
275
|
-
# Run dispatched jobs that add new requests.
|
276
|
-
perform_job(non_block: true, arguments: [@http, @callbacks])
|
277
|
-
end
|
278
|
-
# Try to read data from the SSL socket without blocking and process it.
|
279
|
-
begin
|
280
|
-
@http << @ssl.read_nonblock(1024)
|
281
|
-
rescue IO::WaitReadable
|
282
|
-
end
|
283
|
-
end
|
284
|
-
end
|
357
|
+
stream.headers(request.headers, end_stream: false)
|
358
|
+
stream.data(request.body, end_stream: true)
|
285
359
|
end
|
286
360
|
end
|
287
361
|
end
|
362
|
+
|