lowdown 0.0.5 → 0.1.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/.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.
|