ably 0.7.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.travis.yml +2 -2
  2. data/Rakefile +2 -0
  3. data/SPEC.md +230 -194
  4. data/ably.gemspec +2 -0
  5. data/lib/ably/auth.rb +7 -5
  6. data/lib/ably/models/idiomatic_ruby_wrapper.rb +5 -7
  7. data/lib/ably/models/paginated_resource.rb +14 -21
  8. data/lib/ably/models/protocol_message.rb +1 -1
  9. data/lib/ably/modules/ably.rb +4 -0
  10. data/lib/ably/modules/async_wrapper.rb +2 -2
  11. data/lib/ably/modules/channels_collection.rb +31 -8
  12. data/lib/ably/modules/conversions.rb +10 -0
  13. data/lib/ably/modules/enum.rb +2 -3
  14. data/lib/ably/modules/state_emitter.rb +8 -8
  15. data/lib/ably/modules/state_machine.rb +7 -3
  16. data/lib/ably/realtime/channel.rb +6 -5
  17. data/lib/ably/realtime/channel/channel_manager.rb +11 -10
  18. data/lib/ably/realtime/channel/channel_state_machine.rb +10 -9
  19. data/lib/ably/realtime/channels.rb +3 -0
  20. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +11 -1
  21. data/lib/ably/realtime/connection.rb +55 -16
  22. data/lib/ably/realtime/connection/connection_manager.rb +25 -8
  23. data/lib/ably/realtime/connection/connection_state_machine.rb +9 -9
  24. data/lib/ably/realtime/connection/websocket_transport.rb +2 -2
  25. data/lib/ably/realtime/presence.rb +16 -17
  26. data/lib/ably/util/crypto.rb +1 -1
  27. data/lib/ably/version.rb +1 -1
  28. data/spec/acceptance/realtime/channel_history_spec.rb +6 -5
  29. data/spec/acceptance/realtime/connection_failures_spec.rb +103 -27
  30. data/spec/acceptance/realtime/connection_spec.rb +81 -17
  31. data/spec/acceptance/realtime/presence_spec.rb +82 -30
  32. data/spec/acceptance/rest/auth_spec.rb +22 -19
  33. data/spec/acceptance/rest/client_spec.rb +4 -4
  34. data/spec/acceptance/rest/presence_spec.rb +12 -6
  35. data/spec/rspec_config.rb +9 -0
  36. data/spec/shared/model_behaviour.rb +1 -1
  37. data/spec/spec_helper.rb +4 -1
  38. data/spec/support/event_machine_helper.rb +26 -37
  39. data/spec/support/markdown_spec_formatter.rb +96 -68
  40. data/spec/support/rest_testapp_before_retry.rb +15 -0
  41. data/spec/support/test_app.rb +4 -0
  42. data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +20 -2
  43. data/spec/unit/models/message_spec.rb +1 -1
  44. data/spec/unit/models/paginated_resource_spec.rb +15 -1
  45. data/spec/unit/modules/enum_spec.rb +10 -0
  46. data/spec/unit/realtime/channels_spec.rb +30 -0
  47. data/spec/unit/rest/channels_spec.rb +30 -0
  48. metadata +101 -35
  49. checksums.yaml +0 -7
@@ -15,6 +15,7 @@ module Ably
15
15
  # @param name [String] The name of the channel
16
16
  # @param channel_options [Hash] Channel options, currently reserved for Encryption options
17
17
  # @return [Ably::Realtime::Channel}
18
+ #
18
19
  def get(*args)
19
20
  super
20
21
  end
@@ -27,6 +28,7 @@ module Ably
27
28
  # @yield [options] (optional) if a missing_block is passed to this method and no channel exists matching the name, this block is called
28
29
  # @yieldparam [String] name of the missing channel
29
30
  # @return [Ably::Realtime::Channel]
31
+ #
30
32
  def fetch(*args)
31
33
  super
32
34
  end
@@ -37,6 +39,7 @@ module Ably
37
39
  # the memory footprint of the {Ably::Realtime::Channel Realtime Channel object}. Release channels to free up resources if required
38
40
  #
39
41
  # @return [void]
42
+ #
40
43
  def release(channel)
41
44
  get(channel).detach do
42
45
  @channels.delete(channel)
@@ -54,7 +54,7 @@ module Ably::Realtime
54
54
 
55
55
  when ACTION.Connect
56
56
  when ACTION.Connected
57
- connection.transition_state_machine :connected
57
+ connection.transition_state_machine :connected, protocol_message.error
58
58
 
59
59
  when ACTION.Disconnect, ACTION.Disconnected
60
60
  connection.transition_state_machine :disconnected, protocol_message.error
@@ -118,6 +118,7 @@ module Ably::Realtime
118
118
  def update_connection_recovery_info(protocol_message)
119
119
  if protocol_message.connection_key && (protocol_message.connection_key != connection.key)
120
120
  logger.debug "New connection ID set to #{protocol_message.connection_id} with connection key #{protocol_message.connection_key}"
121
+ detach_attached_channels protocol_message.error if protocol_message.error
121
122
  connection.update_connection_id_and_key protocol_message.connection_id, protocol_message.connection_key
122
123
  end
123
124
 
@@ -126,6 +127,15 @@ module Ably::Realtime
126
127
  end
127
128
  end
128
129
 
130
+ def detach_attached_channels(error)
131
+ channels.select do |channel|
132
+ channel.attached? || channel.attaching?
133
+ end.each do |channel|
134
+ logger.warn "Detaching channel '#{channel.name}': #{error}"
135
+ channel.manager.suspend error
136
+ end
137
+ end
138
+
129
139
  def ack_pending_queue_for_message_serial(ack_protocol_message)
130
140
  drop_pending_queue_from_ack(ack_protocol_message) do |protocol_message|
131
141
  ack_messages protocol_message.messages
@@ -156,7 +156,7 @@ module Ably
156
156
  #
157
157
  # @return [void]
158
158
  #
159
- def ping(&block)
159
+ def ping
160
160
  raise RuntimeError, 'Cannot send a ping when connection is not open' if initialized?
161
161
  raise RuntimeError, 'Cannot send a ping when connection is in a closed or failed state' if closed? || failed?
162
162
 
@@ -166,7 +166,7 @@ module Ably
166
166
  if protocol_message.action == Ably::Models::ProtocolMessage::ACTION.Heartbeat
167
167
  __incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping)
168
168
  time_passed = (Time.now.to_f * 1000 - started.to_f * 1000).to_i
169
- block.call time_passed if block_given?
169
+ yield time_passed if block_given?
170
170
  end
171
171
  end
172
172
 
@@ -177,6 +177,25 @@ module Ably
177
177
  end
178
178
  end
179
179
 
180
+ # @yield [Boolean] True if an internet connection check appears to be up following an HTTP request to a reliable CDN
181
+ # @return [EventMachine::Deferrable]
182
+ # @api private
183
+ def internet_up?
184
+ EventMachine::DefaultDeferrable.new.tap do |deferrable|
185
+ EventMachine::HttpRequest.new(Ably::INTERNET_CHECK.fetch(:url)).get.tap do |http|
186
+ http.errback do
187
+ yield false if block_given?
188
+ deferrable.fail
189
+ end
190
+ http.callback do
191
+ result = http.response_header.status == 200 && http.response.strip == Ably::INTERNET_CHECK.fetch(:ok_text)
192
+ yield result if block_given?
193
+ deferrable.succeed
194
+ end
195
+ end
196
+ end
197
+ end
198
+
180
199
  # @!attribute [r] recovery_key
181
200
  # @return [String] recovery key that can be used by another client to recover this connection with the :recover option
182
201
  def recovery_key
@@ -220,16 +239,31 @@ module Ably
220
239
  @__incoming_protocol_msgbus__ ||= create_pub_sub_message_bus
221
240
  end
222
241
 
223
- # @!attribute [r] host
224
- # @return [String] The host name used for this connection, for network connection failures a {Ably::FALLBACK_HOSTS fallback host} is used to route around networking or intermittent problems
225
- def host
242
+ # Determines the correct host name to use for the next connection attempt and updates current_host
243
+ # @yield [String] The host name used for this connection, for network connection failures a {Ably::FALLBACK_HOSTS fallback host} is used to route around networking or intermittent problems if an Internet connection is available
244
+ # @api private
245
+ def determine_host
246
+ raise ArgumentError, 'Block required' unless block_given?
247
+
226
248
  if can_use_fallback_hosts?
227
- client.fallback_endpoint.host
249
+ internet_up? do |internet_is_up_result|
250
+ @current_host = if internet_is_up_result
251
+ client.fallback_endpoint.host
252
+ else
253
+ client.endpoint.host
254
+ end
255
+ yield current_host
256
+ end
228
257
  else
229
- client.endpoint.host
258
+ @current_host = client.endpoint.host
259
+ yield current_host
230
260
  end
231
261
  end
232
262
 
263
+ # @return [String] The current host that is configured following a call to method {#determine_host}
264
+ # @api private
265
+ attr_reader :current_host
266
+
233
267
  # @!attribute [r] port
234
268
  # @return [Integer] The default port used for this connection
235
269
  def port
@@ -270,8 +304,10 @@ module Ably
270
304
  end
271
305
 
272
306
  # @api private
273
- def create_websocket_transport(&block)
274
- operation = proc do
307
+ def create_websocket_transport
308
+ raise ArgumentError, 'Block required' unless block_given?
309
+
310
+ blocking_operation = proc do
275
311
  URI(client.endpoint).tap do |endpoint|
276
312
  url_params = client.auth.auth_params.merge(
277
313
  timestamp: as_since_epoch(Time.now),
@@ -295,17 +331,20 @@ module Ably
295
331
  end
296
332
 
297
333
  callback = proc do |url|
298
- begin
299
- @transport = EventMachine.connect(host, port, WebsocketTransport, self, url) do |websocket_transport|
300
- yield websocket_transport if block_given?
334
+ determine_host do |host|
335
+ begin
336
+ logger.debug "Connection: Opening socket connection to #{host}:#{port} and URL '#{url}'"
337
+ @transport = EventMachine.connect(host, port, WebsocketTransport, self, url) do |websocket_transport|
338
+ yield websocket_transport if block_given?
339
+ end
340
+ rescue EventMachine::ConnectionError => error
341
+ manager.connection_opening_failed error
301
342
  end
302
- rescue EventMachine::ConnectionError => error
303
- manager.connection_opening_failed error
304
343
  end
305
344
  end
306
345
 
307
346
  # client.auth.auth_params is a blocking call, so defer this into a thread
308
- EventMachine.defer operation, callback
347
+ EventMachine.defer blocking_operation, callback
309
348
  end
310
349
 
311
350
  # @api private
@@ -350,7 +389,7 @@ module Ably
350
389
  end
351
390
 
352
391
  # Simply wait until the next EventMachine tick to ensure Connection initialization is complete
353
- def when_initialized(&block)
392
+ def when_initialized
354
393
  EventMachine.next_tick { yield }
355
394
  end
356
395
 
@@ -47,7 +47,7 @@ module Ably::Realtime
47
47
  #
48
48
  # @yield [Ably::Realtime::Connection::WebsocketTransport] block is called with new websocket transport
49
49
  # @api private
50
- def setup_transport(&block)
50
+ def setup_transport
51
51
  if transport && !transport.ready_for_release?
52
52
  raise RuntimeError, 'Existing WebsocketTransport is connected, and must be closed first'
53
53
  end
@@ -57,14 +57,14 @@ module Ably::Realtime
57
57
  return
58
58
  end
59
59
 
60
- logger.debug "ConnectionManager: Opening connection to #{connection.host}:#{connection.port}"
60
+ logger.debug 'ConnectionManager: Opening a websocket transport connection'
61
61
 
62
62
  connection.create_websocket_transport do |websocket_transport|
63
63
  subscribe_to_transport_events websocket_transport
64
64
  yield websocket_transport if block_given?
65
65
  end
66
66
 
67
- logger.debug 'ConnectionManager: Setting up automatic connection timeout timer for #{TIMEOUTS.fetch(:open)}s'
67
+ logger.debug "ConnectionManager: Setting up automatic connection timeout timer for #{TIMEOUTS.fetch(:open)}s"
68
68
  create_timeout_timer_whilst_in_state(:connect, TIMEOUTS.fetch(:open)) do
69
69
  connection_opening_failed Ably::Exceptions::ConnectionTimeoutError.new("Connection to Ably timed out after #{TIMEOUTS.fetch(:open)}s", nil, 80014)
70
70
  end
@@ -74,10 +74,18 @@ module Ably::Realtime
74
74
  #
75
75
  # @api private
76
76
  def connection_opening_failed(error)
77
- logger.warn "ConnectionManager: Connection to #{connection.host}:#{connection.port} failed; #{error.message}"
77
+ logger.warn "ConnectionManager: Connection to #{connection.current_host}:#{connection.port} failed; #{error.message}"
78
78
  connection.transition_state_machine next_retry_state, Ably::Exceptions::ConnectionError.new("Connection failed; #{error.message}", nil, 80000)
79
79
  end
80
80
 
81
+ # Called whenever a new connection message is received with an error
82
+ #
83
+ # @api private
84
+ def connected_with_error(error)
85
+ logger.warn "ConnectionManager: Connected with error; #{error.message}"
86
+ connection.trigger :error, error
87
+ end
88
+
81
89
  # Ensures the underlying transport has been disconnected and all event emitter callbacks removed
82
90
  #
83
91
  # @api private
@@ -96,7 +104,7 @@ module Ably::Realtime
96
104
  if !transport || transport.disconnected?
97
105
  setup_transport
98
106
  else
99
- transport.reconnect connection.host, connection.port
107
+ transport.reconnect connection.current_host, connection.port
100
108
  end
101
109
  end
102
110
 
@@ -119,6 +127,15 @@ module Ably::Realtime
119
127
  connection.transition_state_machine :closed
120
128
  end
121
129
 
130
+ # Connection has failed
131
+ #
132
+ # @api private
133
+ def fail(error)
134
+ connection.logger.fatal "ConnectionManager: Connection failed - #{error}"
135
+ connection.manager.destroy_transport
136
+ connection.once(:failed) { connection.trigger :error, error }
137
+ end
138
+
122
139
  # When a connection is disconnected whilst connecting, attempt reconnect and/or set state to :suspended or :failed
123
140
  #
124
141
  # @api private
@@ -192,11 +209,11 @@ module Ably::Realtime
192
209
 
193
210
  # Create a timer that will execute in timeout_in seconds.
194
211
  # If the connection state changes however, cancel the timer
195
- def create_timeout_timer_whilst_in_state(timer_id, timeout_in, &block)
196
- raise 'Block required for timer' unless block_given?
212
+ def create_timeout_timer_whilst_in_state(timer_id, timeout_in)
213
+ raise ArgumentError, 'Block required' unless block_given?
197
214
 
198
215
  timers[timer_id] << EventMachine::Timer.new(timeout_in) do
199
- block.call
216
+ yield
200
217
  end
201
218
  connection.once_state_changed { clear_timers timer_id }
202
219
  end
@@ -40,6 +40,10 @@ module Ably::Realtime
40
40
  connection.manager.reconnect_transport
41
41
  end
42
42
 
43
+ after_transition(to: [:connected]) do |connection, current_transition|
44
+ connection.manager.connected_with_error current_transition.metadata if current_transition.metadata
45
+ end
46
+
43
47
  after_transition(to: [:disconnected, :suspended], from: [:connecting]) do |connection, current_transition|
44
48
  connection.manager.respond_to_transport_disconnected_when_connecting current_transition
45
49
  end
@@ -52,9 +56,8 @@ module Ably::Realtime
52
56
  connection.manager.destroy_transport # never reuse a transport if the connection has failed
53
57
  end
54
58
 
55
- after_transition(to: [:failed]) do |connection, current_transition|
56
- connection.logger.fatal "ConnectionStateMachine: Connection failed - #{current_transition.metadata}"
57
- connection.manager.destroy_transport
59
+ before_transition(to: [:failed]) do |connection, current_transition|
60
+ connection.manager.fail current_transition.metadata
58
61
  end
59
62
 
60
63
  after_transition(to: [:closing], from: [:initialized, :disconnected, :suspended]) do |connection|
@@ -70,12 +73,9 @@ module Ably::Realtime
70
73
  end
71
74
 
72
75
  # Transitions responsible for updating connection#error_reason
73
- before_transition(to: [:disconnected, :suspended, :failed]) do |connection, current_transition|
74
- connection.set_failed_connection_error_reason current_transition.metadata
75
- end
76
-
77
- before_transition(to: [:connected, :closed]) do |connection, current_transition|
78
- connection.set_failed_connection_error_reason nil
76
+ before_transition(to: [:connected, :closed, :disconnected, :suspended, :failed]) do |connection, current_transition|
77
+ reason = current_transition.metadata if is_error_type?(current_transition.metadata)
78
+ connection.set_failed_connection_error_reason reason
79
79
  end
80
80
 
81
81
  private
@@ -128,9 +128,9 @@ module Ably::Realtime
128
128
  end
129
129
  end
130
130
 
131
- def create_timer(period, &block)
131
+ def create_timer(period)
132
132
  @timer = EventMachine::Timer.new(period) do
133
- block.call
133
+ yield
134
134
  end
135
135
  end
136
136
 
@@ -57,7 +57,7 @@ module Ably::Realtime
57
57
  #
58
58
  def enter(options = {}, &success_block)
59
59
  @client_id = options.fetch(:client_id, client_id)
60
- @data = options.fetch(:data, data)
60
+ @data = options.fetch(:data, nil)
61
61
  deferrable = EventMachine::DefaultDeferrable.new
62
62
 
63
63
  raise Ably::Exceptions::Standard.new('Unable to enter presence channel without a client_id', 400, 91000) unless client_id
@@ -115,7 +115,7 @@ module Ably::Realtime
115
115
  # @return (see Presence#enter)
116
116
  #
117
117
  def leave(options = {}, &success_block)
118
- @data = options.fetch(:data) if options.has_key?(:data)
118
+ @data = options.fetch(:data, data) # nil value defaults leave data to existing value
119
119
  deferrable = EventMachine::DefaultDeferrable.new
120
120
 
121
121
  raise Ably::Exceptions::Standard.new('Unable to leave presence channel that is not entered', 400, 91002) unless able_to_leave?
@@ -168,7 +168,7 @@ module Ably::Realtime
168
168
  # @return (see Presence#enter)
169
169
  #
170
170
  def update(options = {}, &success_block)
171
- @data = options.fetch(:data) if options.has_key?(:data)
171
+ @data = options.fetch(:data, nil)
172
172
  deferrable = EventMachine::DefaultDeferrable.new
173
173
 
174
174
  ensure_channel_attached(deferrable) do
@@ -211,9 +211,9 @@ module Ably::Realtime
211
211
  #
212
212
  # @yield [Array<Ably::Models::PresenceMessage>] array of members or the member
213
213
  #
214
- # @return [EventMachine::Deferrable] Deferrable that supports both success (callback) and failure (errback) callback
214
+ # @return [EventMachine::Deferrable] Deferrable that supports both success (callback) and failure (errback) callbacks
215
215
  #
216
- def get(options = {}, &success_block)
216
+ def get(options = {})
217
217
  wait_for_sync = options.fetch(:wait_for_sync, true)
218
218
  deferrable = EventMachine::DefaultDeferrable.new
219
219
 
@@ -222,18 +222,17 @@ module Ably::Realtime
222
222
  members.map { |key, presence| presence }.tap do |filtered_members|
223
223
  filtered_members.keep_if { |presence| presence.connection_id == options[:connection_id] } if options[:connection_id]
224
224
  filtered_members.keep_if { |presence| presence.client_id == options[:client_id] } if options[:client_id]
225
+ end.tap do |current_members|
226
+ yield current_members if block_given?
227
+ deferrable.succeed current_members
225
228
  end
226
229
  end
227
230
 
228
231
  if !wait_for_sync || sync_complete?
229
- result = result_block.call
230
- success_block.call result if block_given?
231
- deferrable.succeed result
232
+ result_block.call
232
233
  else
233
234
  sync_pubsub.once(:done) do
234
- result = result_block.call
235
- success_block.call result if block_given?
236
- deferrable.succeed result
235
+ result_block.call
237
236
  end
238
237
 
239
238
  sync_pubsub.once(:failed) do |error|
@@ -486,7 +485,7 @@ module Ably::Realtime
486
485
 
487
486
  def send_protocol_message_and_transition_state_to(action, options = {}, &success_block)
488
487
  deferrable = options.fetch(:deferrable) { raise ArgumentError, 'option :deferrable is required' }
489
- client_id = options.fetch(:client_id) { raise ArgumentError, 'option :client_id is required' }
488
+ client_id = options.fetch(:client_id) { raise ArgumentError, 'option :client_id is required' }
490
489
  target_state = options.fetch(:target_state, nil)
491
490
  failed_state = options.fetch(:failed_state, nil)
492
491
 
@@ -509,14 +508,14 @@ module Ably::Realtime
509
508
  end
510
509
  end
511
510
 
512
- def deferrable_succeed(deferrable, *args, &block)
513
- block.call self, *args if block_given?
514
- EventMachine.next_tick { deferrable.succeed self, *args } # allow callback to be added to the returned Deferrable
511
+ def deferrable_succeed(deferrable, *args)
512
+ yield self, *args if block_given?
513
+ EventMachine.next_tick { deferrable.succeed self, *args } # allow callback to be added to the returned Deferrable before calling succeed
515
514
  deferrable
516
515
  end
517
516
 
518
- def deferrable_fail(deferrable, *args, &block)
519
- block.call self, *args if block_given?
517
+ def deferrable_fail(deferrable, *args)
518
+ yield self, *args if block_given?
520
519
  EventMachine.next_tick { deferrable.fail self, *args } # allow errback to be added to the returned Deferrable
521
520
  deferrable
522
521
  end
@@ -62,7 +62,7 @@ module Ably::Util
62
62
  # @return [String]
63
63
  #
64
64
  def decrypt(encrypted_payload_with_iv)
65
- raise Ably::Exceptions::EncryptionError, 'iv is missing or not long enough' unless encrypted_payload_with_iv.length >= BLOCK_LENGTH*2
65
+ raise Ably::Exceptions::CipherError, 'iv is missing or not long enough' unless encrypted_payload_with_iv.length >= BLOCK_LENGTH*2
66
66
 
67
67
  iv = encrypted_payload_with_iv.slice(0...BLOCK_LENGTH)
68
68
  encrypted_payload = encrypted_payload_with_iv.slice(BLOCK_LENGTH..-1)
data/lib/ably/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Ably
2
- VERSION = '0.7.0'
2
+ VERSION = '0.7.1'
3
3
  end
@@ -60,8 +60,9 @@ describe Ably::Realtime::Channel, '#history', :event_machine do
60
60
  end
61
61
 
62
62
  context 'with lots of messages published with a single client and channel' do
63
- let(:messages_sent) { 40 }
64
- let(:limit) { 20 }
63
+ let(:messages_sent) { 30 }
64
+ let(:rate_per_second) { 4 }
65
+ let(:limit) { 15 }
65
66
 
66
67
  def ensure_message_history_direction_and_paging_is_correct(direction)
67
68
  channel.history(direction: direction, limit: limit) do |history|
@@ -102,10 +103,10 @@ describe Ably::Realtime::Channel, '#history', :event_machine do
102
103
  end
103
104
  end
104
105
 
105
- context 'in multiple ProtocolMessages' do
106
+ context 'in multiple ProtocolMessages', em_timeout: (30 / 4) + 20 do
106
107
  it 'retrieves limited history forwards with pagination' do
107
108
  messages_sent.times do |index|
108
- EventMachine.add_timer(index.to_f / 10) do
109
+ EventMachine.add_timer(index.to_f / rate_per_second) do
109
110
  channel.publish('event', "history#{index}") do
110
111
  next unless index == messages_sent - 1
111
112
  ensure_message_history_direction_and_paging_is_correct :forwards
@@ -116,7 +117,7 @@ describe Ably::Realtime::Channel, '#history', :event_machine do
116
117
 
117
118
  it 'retrieves limited history backwards with pagination' do
118
119
  messages_sent.times.to_a.reverse.each do |index|
119
- EventMachine.add_timer((messages_sent - index).to_f / 10) do
120
+ EventMachine.add_timer((messages_sent - index).to_f / rate_per_second) do
120
121
  channel.publish('event', "history#{index}") do
121
122
  next unless index == 0
122
123
  ensure_message_history_direction_and_paging_is_correct :backwards if index == 0