lowdown 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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.