lowdown 0.0.5 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/README.md +7 -0
- data/lib/lowdown.rb +4 -1
- data/lib/lowdown/client.rb +2 -0
- data/lib/lowdown/connection.rb +174 -86
- data/lib/lowdown/threading.rb +0 -43
- data/lib/lowdown/version.rb +1 -3
- data/lowdown.gemspec +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3bae6b12ffe8101e10ab41c87a7e58f191d1c9e6
|
4
|
+
data.tar.gz: c34a58e8561c4c65c2292b207800b63237fcd038
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 13de1cb6266a8a507228b3ded7443c098fd7582d4df3c52c6dfaaba71c6cd1ec7b7db31d365a441187d3ecf88bbbe9485379d68377ae0b1563fc17373f857c4a
|
7
|
+
data.tar.gz: 32b3ae325cf9029554044f7bc63e5dd2a84b00a529b861c430084eb3d0904e2f30fc8d6ffacaeb2547457d972619aefbefbb7299124072ae175b329223c95bb0
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -8,6 +8,13 @@ Lowdown is a Ruby client for the HTTP/2 version of the Apple Push Notification S
|
|
8
8
|
|
9
9
|
Multiple notifications are multiplexed for efficiency.
|
10
10
|
|
11
|
+
If you need to cotinuously send notifications, it’s a good idea to keep an open connection. Managing that, in for
|
12
|
+
instance a daemon, is beyond the scope of this library. We might release an extra daemon/server tool in the future that
|
13
|
+
provides this functionality, but for now you should simply use the Client provided in this library without the block
|
14
|
+
form (which automatically closes the connection) and build your own daemon/server setup, as required.
|
15
|
+
|
16
|
+
Also checkout [this library](https://github.com/alloy/time_zone_scheduler) for scheduling across time zones.
|
17
|
+
|
11
18
|
NOTE: _It is not yet battle-tested. This will all follow over the next few weeks._
|
12
19
|
|
13
20
|
## Installation
|
data/lib/lowdown.rb
CHANGED
@@ -3,7 +3,10 @@ require "lowdown/version"
|
|
3
3
|
|
4
4
|
# Lowdown is a Ruby client for the HTTP/2 version of the Apple Push Notification Service.
|
5
5
|
#
|
6
|
-
# Multiple notifications are multiplexed for efficiency.
|
6
|
+
# Multiple notifications are multiplexed and responses are yielded onto a different thread for efficiency.
|
7
|
+
#
|
8
|
+
# Note that it is thus _your_ responsibility to take the threading issue into account. E.g. if you are planning to
|
9
|
+
# update a DB model with the status of a notification delivery, be sure to respect the treading rules of your DB client.
|
7
10
|
#
|
8
11
|
# The main classes you will interact with are {Lowdown::Client} and {Lowdown::Notification}. For testing purposes there
|
9
12
|
# are some helpers available in {Lowdown::Mock}.
|
data/lib/lowdown/client.rb
CHANGED
data/lib/lowdown/connection.rb
CHANGED
@@ -2,17 +2,20 @@ require "lowdown/threading"
|
|
2
2
|
require "lowdown/response"
|
3
3
|
|
4
4
|
require "http/2"
|
5
|
+
|
5
6
|
require "openssl"
|
6
|
-
require "uri"
|
7
7
|
require "socket"
|
8
|
+
require "timeout"
|
9
|
+
require "uri"
|
8
10
|
|
9
11
|
if HTTP2::VERSION == "0.8.0"
|
10
12
|
# @!visibility private
|
11
13
|
#
|
12
|
-
#
|
14
|
+
# This monkey-patch ensures that we send the HTTP/2 connection preface before anything else.
|
15
|
+
#
|
16
|
+
# @see https://github.com/igrigorik/http-2/pull/44
|
17
|
+
#
|
13
18
|
class HTTP2::Client
|
14
|
-
# This monkey-patch ensures that we send the HTTP/2 connection preface before anything else.
|
15
|
-
#
|
16
19
|
def connection_management(frame)
|
17
20
|
if @state == :waiting_connection_preface
|
18
21
|
send_connection_preface
|
@@ -22,6 +25,22 @@ if HTTP2::VERSION == "0.8.0"
|
|
22
25
|
end
|
23
26
|
end
|
24
27
|
end
|
28
|
+
|
29
|
+
# @!visibility private
|
30
|
+
#
|
31
|
+
# These monkey-patches ensure that data added to a buffer has a binary encoding, as to not lead to encoding clashes.
|
32
|
+
#
|
33
|
+
# @see https://github.com/igrigorik/http-2/pull/46
|
34
|
+
#
|
35
|
+
class HTTP2::Buffer
|
36
|
+
def <<(x)
|
37
|
+
super(x.force_encoding(Encoding::BINARY))
|
38
|
+
end
|
39
|
+
|
40
|
+
def prepend(x)
|
41
|
+
super(x.force_encoding(Encoding::BINARY))
|
42
|
+
end
|
43
|
+
end
|
25
44
|
end
|
26
45
|
|
27
46
|
module Lowdown
|
@@ -55,33 +74,9 @@ module Lowdown
|
|
55
74
|
# @return [void]
|
56
75
|
#
|
57
76
|
def open
|
58
|
-
|
59
|
-
|
60
|
-
@
|
61
|
-
@ssl.sync_close = true
|
62
|
-
@ssl.hostname = @uri.hostname
|
63
|
-
@ssl.connect
|
64
|
-
|
65
|
-
@http = HTTP2::Client.new
|
66
|
-
@http.on(:frame) do |bytes|
|
67
|
-
@ssl.print(bytes)
|
68
|
-
@ssl.flush
|
69
|
-
end
|
70
|
-
|
71
|
-
@main_queue = Threading::DispatchQueue.new
|
72
|
-
@work_queue = Threading::DispatchQueue.new
|
73
|
-
@requests = Threading::Counter.new
|
74
|
-
@exceptions = Queue.new
|
75
|
-
@worker_thread = start_worker_thread!
|
76
|
-
end
|
77
|
-
|
78
|
-
# @return [Boolean]
|
79
|
-
# whether or not the Connection is open.
|
80
|
-
#
|
81
|
-
# @todo Possibly add a HTTP/2 `PING` in the future.
|
82
|
-
#
|
83
|
-
def open?
|
84
|
-
!@ssl.nil? && !@ssl.closed?
|
77
|
+
raise "Connection already open." if @worker
|
78
|
+
@requests = Threading::Counter.new
|
79
|
+
@worker = Worker.new(@uri, @ssl_context)
|
85
80
|
end
|
86
81
|
|
87
82
|
# Flushes the connection, terminates the worker thread, and closes the socket. Finally it peforms one more check for
|
@@ -90,17 +85,33 @@ module Lowdown
|
|
90
85
|
# @return [void]
|
91
86
|
#
|
92
87
|
def close
|
88
|
+
return unless @worker
|
93
89
|
flush
|
90
|
+
@worker.stop
|
91
|
+
@worker = @requests = nil
|
92
|
+
end
|
94
93
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
94
|
+
# This performs a HTTP/2 PING to determine if the connection is actually alive.
|
95
|
+
#
|
96
|
+
# @param [Numeric] timeout
|
97
|
+
# the maximum amount of time to wait for the service to reply to the PING.
|
98
|
+
#
|
99
|
+
# @return [Boolean]
|
100
|
+
# whether or not the Connection is open.
|
101
|
+
#
|
102
|
+
def open?(timeout = 5)
|
103
|
+
return false unless @worker
|
104
|
+
Timeout.timeout(timeout) do
|
105
|
+
caller_thread = Thread.current
|
106
|
+
@worker.enqueue do |http|
|
107
|
+
http.ping('12345678') { caller_thread.run }
|
108
|
+
end
|
109
|
+
Thread.stop
|
110
|
+
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
|
104
115
|
end
|
105
116
|
|
106
117
|
# Halts the calling thread until all dispatched requests have been performed.
|
@@ -108,14 +119,14 @@ module Lowdown
|
|
108
119
|
# @return [void]
|
109
120
|
#
|
110
121
|
def flush
|
111
|
-
|
112
|
-
|
113
|
-
sleep 0.1
|
114
|
-
end
|
122
|
+
return unless @worker
|
123
|
+
sleep 0.1 until !@worker.alive? || @worker.empty? && @requests.zero?
|
115
124
|
end
|
116
125
|
|
117
126
|
# Sends the provided data as a `POST` request to the service.
|
118
127
|
#
|
128
|
+
# @note The callback is performed on a different thread, dedicated to perfoming these callbacks.
|
129
|
+
#
|
119
130
|
# @param [String] path
|
120
131
|
# the request path, which should be `/3/device/<device-token>`.
|
121
132
|
#
|
@@ -141,11 +152,11 @@ module Lowdown
|
|
141
152
|
|
142
153
|
def request(method, path, custom_headers, body, &callback)
|
143
154
|
@requests.increment!
|
144
|
-
@
|
155
|
+
@worker.enqueue do |http, callbacks|
|
145
156
|
headers = { ":method" => method.to_s, ":path" => path.to_s, "content-length" => body.bytesize.to_s }
|
146
157
|
custom_headers.each { |k, v| headers[k] = v.to_s }
|
147
158
|
|
148
|
-
stream =
|
159
|
+
stream = http.new_stream
|
149
160
|
response = Response.new
|
150
161
|
|
151
162
|
stream.on(:headers) do |response_headers|
|
@@ -158,7 +169,7 @@ module Lowdown
|
|
158
169
|
end
|
159
170
|
|
160
171
|
stream.on(:close) do
|
161
|
-
|
172
|
+
callbacks << lambda do
|
162
173
|
callback.call(response)
|
163
174
|
@requests.decrement!
|
164
175
|
end
|
@@ -167,54 +178,131 @@ module Lowdown
|
|
167
178
|
stream.headers(headers, end_stream: false)
|
168
179
|
stream.data(body, end_stream: true)
|
169
180
|
end
|
170
|
-
|
171
|
-
# The caller might be posting many notifications, so use this time to also dispatch work onto the main thread.
|
172
|
-
@main_queue.drain!
|
173
181
|
end
|
174
182
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
183
|
+
# @!visibility private
|
184
|
+
#
|
185
|
+
# Creates a new worker thread which maintains all its own state:
|
186
|
+
# * SSL connection
|
187
|
+
# * HTTP2 client
|
188
|
+
# * Another thread from where request callbacks are ran
|
189
|
+
#
|
190
|
+
class Worker < Thread
|
191
|
+
def initialize(uri, ssl_context)
|
192
|
+
@uri, @ssl_context = uri, ssl_context
|
185
193
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
194
|
+
# 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
|
195
|
+
# that any attempt to add a new job to the queue is going to halt the calling thread *until* we change the max.
|
196
|
+
@queue = SizedQueue.new(1)
|
197
|
+
@queue << lambda { |*_| }
|
198
|
+
|
199
|
+
# Setup the consumer that performs the callbacks passed to Connection#request
|
200
|
+
@callback_queue = Queue.new
|
201
|
+
@callback_thread = Thread.new(@callback_queue) { |q| loop { q.pop.call } }
|
202
|
+
|
203
|
+
# Store the caller thread to be able to resume it once connected and to send exceptions to.
|
204
|
+
@caller_thread = Thread.current
|
205
|
+
# Start the worker thread.
|
206
|
+
super(&method(:main))
|
207
|
+
# Put caller thread into sleep until connected.
|
208
|
+
Thread.stop
|
209
|
+
end
|
210
|
+
|
211
|
+
# @yield [http, callbacks_queue]
|
212
|
+
#
|
213
|
+
# @yieldparam [HTTP2::Client] http
|
214
|
+
# the HTTP2 client instance.
|
215
|
+
#
|
216
|
+
# @yieldparam [Queue] callbacks_queue
|
217
|
+
# the queue on which request callbacks should be performed.
|
218
|
+
#
|
219
|
+
# @return [void]
|
220
|
+
#
|
221
|
+
def enqueue(&job)
|
222
|
+
@queue << job
|
223
|
+
end
|
224
|
+
|
225
|
+
# @return [Boolean]
|
226
|
+
# whether or not the work queue is empty.
|
227
|
+
#
|
228
|
+
def empty?
|
229
|
+
@queue.empty?
|
230
|
+
end
|
231
|
+
|
232
|
+
# Tells the runloop to stop and halts the caller until finished.
|
233
|
+
#
|
234
|
+
# @return [void]
|
235
|
+
#
|
236
|
+
def stop
|
237
|
+
self[:should_exit] = true
|
238
|
+
join
|
239
|
+
end
|
240
|
+
|
241
|
+
private
|
197
242
|
|
198
|
-
|
199
|
-
|
243
|
+
def main
|
244
|
+
connect
|
245
|
+
runloop
|
246
|
+
rescue Exception => exception
|
247
|
+
# Send any unexpected exceptions back to the thread that started the loop.
|
248
|
+
@caller_thread.raise(exception)
|
249
|
+
ensure
|
250
|
+
cleanup
|
251
|
+
end
|
252
|
+
|
253
|
+
def cleanup
|
254
|
+
@callback_thread.kill
|
255
|
+
@ssl.close
|
256
|
+
end
|
257
|
+
|
258
|
+
def connect
|
259
|
+
@ssl = OpenSSL::SSL::SSLSocket.new(TCPSocket.new(@uri.host, @uri.port), @ssl_context)
|
260
|
+
@ssl.sync_close = true
|
261
|
+
@ssl.hostname = @uri.hostname
|
262
|
+
@ssl.connect
|
263
|
+
|
264
|
+
@http = HTTP2::Client.new
|
265
|
+
@http.on(:frame) do |bytes|
|
266
|
+
# This is going to be performed on the worker thread and thus does *not* write to @ssl from another thread than
|
267
|
+
# the thread it’s being read from.
|
268
|
+
@ssl.print(bytes)
|
269
|
+
@ssl.flush
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def change_to_connected_state
|
274
|
+
@queue.max = @http.remote_settings[:settings_max_concurrent_streams]
|
275
|
+
@connected = true
|
276
|
+
@caller_thread.run
|
277
|
+
end
|
278
|
+
|
279
|
+
# @note Only made into a method so it can be overriden from the tests, because our test setup doesn’t behave the
|
280
|
+
# same as the real APNS service.
|
281
|
+
#
|
282
|
+
def http_connected?
|
283
|
+
@http.state == :connected
|
284
|
+
end
|
285
|
+
|
286
|
+
# Start the main IO and HTTP processing loop.
|
287
|
+
def runloop
|
288
|
+
until self[:should_exit] || @ssl.closed?
|
289
|
+
# Once connected, add requests while the max stream count has not yet been reached.
|
290
|
+
if !@connected
|
291
|
+
change_to_connected_state if http_connected?
|
292
|
+
elsif @http.active_stream_count < @queue.max
|
200
293
|
begin
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
exception_occurred_in_worker(exception)
|
294
|
+
# Run dispatched jobs that add new requests.
|
295
|
+
@queue.pop(true).call(@http, @callback_queue)
|
296
|
+
rescue ThreadError
|
205
297
|
end
|
206
298
|
end
|
299
|
+
# Try to read data from the SSL socket without blocking and process it.
|
300
|
+
begin
|
301
|
+
@http << @ssl.read_nonblock(1024)
|
302
|
+
rescue IO::WaitReadable
|
303
|
+
end
|
207
304
|
end
|
208
305
|
end
|
209
306
|
end
|
210
|
-
|
211
|
-
# Raise the exception on the main thread and reset the number of in-flight requests so that a potential blocked
|
212
|
-
# caller of `Connection#flush` will continue.
|
213
|
-
#
|
214
|
-
def exception_occurred_in_worker(exception)
|
215
|
-
@exceptions << exception
|
216
|
-
@main_queue.dispatch { raise @exceptions.pop }
|
217
|
-
@requests.value = @http.active_stream_count
|
218
|
-
end
|
219
307
|
end
|
220
308
|
end
|
data/lib/lowdown/threading.rb
CHANGED
@@ -4,49 +4,6 @@ module Lowdown
|
|
4
4
|
# A collection of internal threading related helpers.
|
5
5
|
#
|
6
6
|
module Threading
|
7
|
-
# A queue of blocks that are to be dispatched onto another thread.
|
8
|
-
#
|
9
|
-
class DispatchQueue
|
10
|
-
def initialize
|
11
|
-
@queue = Queue.new
|
12
|
-
end
|
13
|
-
|
14
|
-
# Adds a block to the queue.
|
15
|
-
#
|
16
|
-
# @return [void]
|
17
|
-
#
|
18
|
-
def dispatch(&block)
|
19
|
-
@queue << block
|
20
|
-
end
|
21
|
-
|
22
|
-
# @return [Boolean]
|
23
|
-
# whether or not the queue is empty.
|
24
|
-
#
|
25
|
-
def empty?
|
26
|
-
@queue.empty?
|
27
|
-
end
|
28
|
-
|
29
|
-
# Performs the number of dispatched blocks that were on the queue at the moment of calling `#drain!`. Unlike
|
30
|
-
# performing blocks _until the queue is empty_, this ensures that it doesn’t block the calling thread too long if
|
31
|
-
# another thread is dispatching more work at the same time.
|
32
|
-
#
|
33
|
-
# By default this will let any exceptions bubble up on the main thread or catch and return them on other threads.
|
34
|
-
#
|
35
|
-
# @param [Boolean] rescue_exceptions
|
36
|
-
# whether or not to rescue exceptions.
|
37
|
-
#
|
38
|
-
# @return [Exception, nil]
|
39
|
-
# in case of rescueing exceptions, this returns the exception raised during execution of a block.
|
40
|
-
#
|
41
|
-
def drain!(rescue_exceptions = (Thread.current != Thread.main))
|
42
|
-
@queue.size.times { @queue.pop.call }
|
43
|
-
nil
|
44
|
-
rescue Exception => exception
|
45
|
-
raise unless rescue_exceptions
|
46
|
-
exception
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
7
|
# A simple thread-safe counter.
|
51
8
|
#
|
52
9
|
class Counter
|
data/lib/lowdown/version.rb
CHANGED
data/lowdown.gemspec
CHANGED
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
21
|
# This is currently set to >= 2.0.0 in the http-2 gemspec, which is incorrect, as it uses required keyword arguments.
|
22
|
-
spec.required_ruby_version = '>= 2.1.
|
22
|
+
spec.required_ruby_version = '>= 2.1.1'
|
23
23
|
|
24
24
|
spec.add_runtime_dependency "http-2", ">= 0.8"
|
25
25
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lowdown
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eloy Durán
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-01-
|
11
|
+
date: 2016-01-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: http-2
|
@@ -106,7 +106,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
106
106
|
requirements:
|
107
107
|
- - ">="
|
108
108
|
- !ruby/object:Gem::Version
|
109
|
-
version: 2.1.
|
109
|
+
version: 2.1.1
|
110
110
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
111
111
|
requirements:
|
112
112
|
- - ">="
|
@@ -114,7 +114,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
114
114
|
version: '0'
|
115
115
|
requirements: []
|
116
116
|
rubyforge_project:
|
117
|
-
rubygems_version: 2.
|
117
|
+
rubygems_version: 2.2.2
|
118
118
|
signing_key:
|
119
119
|
specification_version: 4
|
120
120
|
summary: A Ruby client for the HTTP/2 version of the Apple Push Notification Service.
|