lowdown 0.1.0 → 0.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3bae6b12ffe8101e10ab41c87a7e58f191d1c9e6
4
- data.tar.gz: c34a58e8561c4c65c2292b207800b63237fcd038
3
+ metadata.gz: 044f0dfd72c7b5a984abac1cd844e06b71b1fac1
4
+ data.tar.gz: 68eae4bb641cf6d39edaf59681060f6b7ded8089
5
5
  SHA512:
6
- metadata.gz: 13de1cb6266a8a507228b3ded7443c098fd7582d4df3c52c6dfaaba71c6cd1ec7b7db31d365a441187d3ecf88bbbe9485379d68377ae0b1563fc17373f857c4a
7
- data.tar.gz: 32b3ae325cf9029554044f7bc63e5dd2a84b00a529b861c430084eb3d0904e2f30fc8d6ffacaeb2547457d972619aefbefbb7299124072ae175b329223c95bb0
6
+ metadata.gz: 65c0917420183c3408930d60645e00d54ffcf653bb3f28aa61aa00a8a85f623213652fbb7b8eded3c6524caa8025c57e488f8c5a58cd7786333e8b3eba0fed23
7
+ data.tar.gz: 2e20f00c914f2a95efff4a0a017f768004c892fdce49549b8aa269d30d2e431575c7b66e83103a4f3beb7af3a1c63dff6144955f6130e46b209677cbbf0df5f2
data/.yardopts CHANGED
@@ -1,3 +1,4 @@
1
+ --protected
1
2
  --no-private
2
3
  --markup markdown
3
4
  --main README.md
data/Rakefile CHANGED
@@ -19,6 +19,7 @@ begin
19
19
 
20
20
  require "rake/testtask"
21
21
  Rake::TestTask.new(:test) do |t|
22
+ t.options = "--verbose"
22
23
  t.libs << "test"
23
24
  t.libs << "lib"
24
25
  t.test_files = FileList['test/**/*_test.rb']
@@ -160,8 +160,14 @@ module Lowdown
160
160
  # @param [Notification] notification
161
161
  # the notification object whose data to send to the service.
162
162
  #
163
- # @yield (see Connection#post)
164
- # @yieldparam (see Connection#post)
163
+ # @yield [response, notification]
164
+ # called when the request is finished and a response is available.
165
+ #
166
+ # @yieldparam [Response] response
167
+ # the Response that holds the status data that came back from the service.
168
+ #
169
+ # @yieldparam [Notification] notification
170
+ # the originally passed in notification object.
165
171
  #
166
172
  # @raise [ArgumentError]
167
173
  # raised if the Notification is not {Notification#valid?}.
@@ -180,7 +186,10 @@ module Lowdown
180
186
 
181
187
  body = notification.formatted_payload.to_json
182
188
 
183
- @connection.post("/3/device/#{notification.token}", headers, body, &callback)
189
+ # No need to keep a strong reference to the notification object, unless the user really wants it.
190
+ actual_callback = callback.arity < 2 ? callback : lambda { |response| callback.call(response, notification) }
191
+
192
+ @connection.post("/3/device/#{notification.token}", headers, body, &actual_callback)
184
193
  end
185
194
  end
186
195
  end
@@ -104,7 +104,7 @@ module Lowdown
104
104
  Timeout.timeout(timeout) do
105
105
  caller_thread = Thread.current
106
106
  @worker.enqueue do |http|
107
- http.ping('12345678') { caller_thread.run }
107
+ http.ping('whatever') { caller_thread.run }
108
108
  end
109
109
  Thread.stop
110
110
  end
@@ -120,7 +120,7 @@ module Lowdown
120
120
  #
121
121
  def flush
122
122
  return unless @worker
123
- sleep 0.1 until !@worker.alive? || @worker.empty? && @requests.zero?
123
+ sleep 0.1 until !@worker.working? && @requests.zero?
124
124
  end
125
125
 
126
126
  # Sends the provided data as a `POST` request to the service.
@@ -137,10 +137,10 @@ module Lowdown
137
137
  # the (JSON) encoded payload data to send to the service.
138
138
  #
139
139
  # @yield [response]
140
- # Called when the request is finished and a response is available.
140
+ # called when the request is finished and a response is available.
141
141
  #
142
142
  # @yieldparam [Response] response
143
- # The Response that holds the status data that came back from the service.
143
+ # the Response that holds the status data that came back from the service.
144
144
  #
145
145
  # @return [void]
146
146
  #
@@ -169,7 +169,7 @@ module Lowdown
169
169
  end
170
170
 
171
171
  stream.on(:close) do
172
- callbacks << lambda do
172
+ callbacks.enqueue do
173
173
  callback.call(response)
174
174
  @requests.decrement!
175
175
  end
@@ -187,75 +187,49 @@ module Lowdown
187
187
  # * HTTP2 client
188
188
  # * Another thread from where request callbacks are ran
189
189
  #
190
- class Worker < Thread
190
+ class Worker < Threading::Consumer
191
191
  def initialize(uri, ssl_context)
192
192
  @uri, @ssl_context = uri, ssl_context
193
193
 
194
+ # Start the worker thread.
195
+ #
194
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
195
197
  # 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))
198
+ super(queue: Thread::SizedQueue.new(1))
207
199
  # Put caller thread into sleep until connected.
208
200
  Thread.stop
209
201
  end
210
202
 
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.
203
+ # Tells the runloop to stop and halts the caller until finished.
218
204
  #
219
205
  # @return [void]
220
206
  #
221
- def enqueue(&job)
222
- @queue << job
207
+ def stop
208
+ thread[:should_exit] = true
209
+ thread.join
223
210
  end
224
211
 
225
212
  # @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.
213
+ # whether or not the worker is still alive and kicking.
233
214
  #
234
- # @return [void]
235
- #
236
- def stop
237
- self[:should_exit] = true
238
- join
215
+ def working?
216
+ alive? && !empty? && !@callbacks.empty?
239
217
  end
240
218
 
241
219
  private
242
220
 
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
221
+ def post_runloop
222
+ @callbacks.kill
255
223
  @ssl.close
224
+ super
256
225
  end
257
226
 
258
- def connect
227
+ def pre_runloop
228
+ super
229
+
230
+ # Setup the request callbacks consumer here so its parent thread will be this worker thread.
231
+ @callbacks = Threading::Consumer.new
232
+
259
233
  @ssl = OpenSSL::SSL::SSLSocket.new(TCPSocket.new(@uri.host, @uri.port), @ssl_context)
260
234
  @ssl.sync_close = true
261
235
  @ssl.hostname = @uri.hostname
@@ -270,31 +244,36 @@ module Lowdown
270
244
  end
271
245
  end
272
246
 
247
+ # Called when the HTTP client changes its state to `:connected` and lets the parent thread (which was stopped in
248
+ # `#initialize`) continue.
249
+ #
250
+ # @return [void]
251
+ #
273
252
  def change_to_connected_state
274
- @queue.max = @http.remote_settings[:settings_max_concurrent_streams]
253
+ queue.max = @http.remote_settings[:settings_max_concurrent_streams]
275
254
  @connected = true
276
- @caller_thread.run
255
+ parent_thread.run
277
256
  end
278
257
 
279
258
  # @note Only made into a method so it can be overriden from the tests, because our test setup doesn’t behave the
280
259
  # same as the real APNS service.
281
260
  #
261
+ # @return [Boolean]
262
+ # whether or not the HTTP client’s state is `:connected`.
263
+ #
282
264
  def http_connected?
283
265
  @http.state == :connected
284
266
  end
285
267
 
286
268
  # Start the main IO and HTTP processing loop.
287
269
  def runloop
288
- until self[:should_exit] || @ssl.closed?
270
+ until thread[:should_exit] || @ssl.closed?
289
271
  # Once connected, add requests while the max stream count has not yet been reached.
290
272
  if !@connected
291
273
  change_to_connected_state if http_connected?
292
- elsif @http.active_stream_count < @queue.max
293
- begin
294
- # Run dispatched jobs that add new requests.
295
- @queue.pop(true).call(@http, @callback_queue)
296
- rescue ThreadError
297
- end
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])
298
277
  end
299
278
  # Try to read data from the SSL socket without blocking and process it.
300
279
  begin
data/lib/lowdown/mock.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require "lowdown/certificate"
2
2
  require "lowdown/client"
3
3
  require "lowdown/response"
4
+ require "lowdown/threading"
4
5
 
5
6
  module Lowdown
6
7
  # Provides a collection of test helpers.
@@ -122,17 +123,18 @@ module Lowdown
122
123
 
123
124
  # @!group Real API: Instance Method Summary
124
125
 
125
- # Yields stubbed {#responses} or if none are available defaults to success responses.
126
+ # Yields stubbed {#responses} or if none are available defaults to success responses. It does this on a different
127
+ # thread, just like the real API does.
126
128
  #
127
129
  # @param (see Lowdown::Connection#post)
128
130
  # @yield (see Lowdown::Connection#post)
129
131
  # @yieldparam (see Lowdown::Connection#post)
130
132
  # @return (see Lowdown::Connection#post)
131
133
  #
132
- def post(path, headers, body)
134
+ def post(path, headers, body, &callback)
133
135
  response = @responses.shift || Response.new(":status" => "200", "apns-id" => (headers["apns-id"] || generate_id))
134
136
  @requests << Request.new(path, headers, body, response)
135
- yield response
137
+ @callbacks.enqueue { callback.call(response) }
136
138
  end
137
139
 
138
140
  # Changes {#open?} to return `true`.
@@ -140,6 +142,7 @@ module Lowdown
140
142
  # @return [void]
141
143
  #
142
144
  def open
145
+ @callbacks = Threading::Consumer.new
143
146
  @open = true
144
147
  end
145
148
 
@@ -148,6 +151,9 @@ module Lowdown
148
151
  # @return [void]
149
152
  #
150
153
  def close
154
+ flush
155
+ @callbacks.kill
156
+ @callbacks = nil
151
157
  @open = false
152
158
  end
153
159
 
@@ -156,6 +162,12 @@ module Lowdown
156
162
  # @return [void]
157
163
  #
158
164
  def flush
165
+ caller_thread = Thread.current
166
+ @callbacks.enqueue do
167
+ sleep 0.1
168
+ caller_thread.run
169
+ end
170
+ Thread.stop
159
171
  end
160
172
 
161
173
  # @return (see Lowdown::Connection#open?)
@@ -4,6 +4,132 @@ module Lowdown
4
4
  # A collection of internal threading related helpers.
5
5
  #
6
6
  module Threading
7
+ # This class performs jobs on a private thread, provides lifecycle callbacks, and sends exceptions onto its parent
8
+ # thread.
9
+ #
10
+ class Consumer
11
+ # @param [Thread::Queue] queue
12
+ # a queue instance. Provide a `Thread::SizedQueue` if you want queue of a max size.
13
+ #
14
+ # @param [Thread] parent_thread
15
+ # the thread to send exceptions to.
16
+ #
17
+ def initialize(queue: Thread::Queue.new, parent_thread: Thread.current)
18
+ @queue, @parent_thread = queue, parent_thread
19
+ @thread = Thread.new(&method(:main))
20
+ end
21
+
22
+ # Schedules a job to be performed.
23
+ #
24
+ # @return [void]
25
+ #
26
+ def enqueue(&job)
27
+ queue << job
28
+ end
29
+
30
+ # Kills the private thread.
31
+ #
32
+ # @return [void]
33
+ #
34
+ def kill
35
+ thread.kill
36
+ end
37
+
38
+ # @return [Boolean]
39
+ # whether or not the private thread is still alive.
40
+ #
41
+ def alive?
42
+ thread.alive?
43
+ end
44
+
45
+ # @return [Boolean]
46
+ # whether or not there are any scheduled jobs left in the queue.
47
+ #
48
+ def empty?
49
+ queue.empty?
50
+ end
51
+
52
+ protected
53
+
54
+ # @return [Thread]
55
+ # the private thread.
56
+ #
57
+ attr_reader :thread
58
+
59
+ # @return [Thread]
60
+ # the thread to send exceptions to.
61
+ #
62
+ attr_reader :parent_thread
63
+
64
+ # @return [Thread::Queue]
65
+ # the jobs queue.
66
+ #
67
+ attr_reader :queue
68
+
69
+ # This represents the full lifecycle of the consumer thread. It performs the individual events, catches uncaught
70
+ # exceptions and sends those to the parent thread, and performs cleanup.
71
+ #
72
+ # Subclasses should override the individual events.
73
+ #
74
+ # @note This method is ran on the private thread.
75
+ #
76
+ # @return [void]
77
+ #
78
+ def main
79
+ pre_runloop
80
+ runloop
81
+ rescue Exception => exception
82
+ parent_thread.raise(exception)
83
+ ensure
84
+ post_runloop
85
+ end
86
+
87
+ # Ran _before_ any jobs are performed.
88
+ #
89
+ # @note (see #main)
90
+ #
91
+ # @return [void]
92
+ #
93
+ def pre_runloop
94
+ end
95
+
96
+ # The loop that performs scheduled jobs.
97
+ #
98
+ # @note (see #main)
99
+ #
100
+ # @return [void]
101
+ #
102
+ def runloop
103
+ loop { perform_job(non_block: false) }
104
+ end
105
+
106
+ # Ran when the thread is killed or an uncaught exception occurred.
107
+ #
108
+ # This kills the consumer thread, which means that any cleanup you need to perform should be done *before* calling
109
+ # this `super` implementation.
110
+ #
111
+ # @note (see #main)
112
+ #
113
+ # @return [void]
114
+ #
115
+ def post_runloop
116
+ thread.kill
117
+ end
118
+
119
+ # @param [Boolean] non_block
120
+ # whether or not the thread should be halted if there are no jobs to perform.
121
+ #
122
+ # @param [Array<Object>] arguments
123
+ # arguments that should be passed to the invoked job.
124
+ #
125
+ # @return [void]
126
+ #
127
+ def perform_job(non_block:, arguments: nil)
128
+ queue.pop(non_block).call(*arguments)
129
+ rescue ThreadError
130
+ end
131
+ end
132
+
7
133
  # A simple thread-safe counter.
8
134
  #
9
135
  class Counter
@@ -1,3 +1,3 @@
1
1
  module Lowdown
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
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.1.0
4
+ version: 0.1.1
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-17 00:00:00.000000000 Z
11
+ date: 2016-01-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http-2
@@ -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.2.2
117
+ rubygems_version: 2.4.5.1
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.