lightstreamer 0.8 → 0.9

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: 2fbedc6ddd408cf66740c82acb37a83fb1954b13
4
- data.tar.gz: e259733aa9ab2bd100a1cee9a58ea120de44614c
3
+ metadata.gz: 75687f5d599512c6623967cd0e005f67c616e3f2
4
+ data.tar.gz: b8eadafa92da0812fa59803e44b371d4537aeedb
5
5
  SHA512:
6
- metadata.gz: a47999d9791bf4eb11671fadf6ed949ff3bed475d7856ce2ae1a9f99dceba78608b52ac630ba58a5425a1154c6807ab9aa7e2937119f3e8136051a81be82be84
7
- data.tar.gz: 689b06550c0ff39e08ec1042a80e6aa99ab47ddeaa92c0acd6f064eef6bcb6e9be202b90a19627c878157e3d41c795e1b131245ed0538364faf2e15d9dfff5cf
6
+ metadata.gz: e8a46f18e3bd81a5635c660caea2911e1f04f5503476293e37bebd49bf0a45a3f28a980bc546b8d316a419aad5c0b550d5134e1dab62ff7a513cf81fe20299e7
7
+ data.tar.gz: 273ec4437edcde9a079a0f7c51e83701751caae948a776c623d89443ed7f9f25eb556c9255fda11d44e1c4b76785fae93b19d19b56d731938dd0e3eb34ad129e
data/CHANGELOG.md CHANGED
@@ -1,10 +1,16 @@
1
1
  # Lightstreamer Changelog
2
2
 
3
+ ### 0.9 — August 4, 2016
4
+
5
+ - Replaced the `Lightstreamer::Session#error` attribute with a new `Lightstreamer::Session#on_error` callback to enable
6
+ more efficient error handling and notification
7
+ - Fixed issues with `Lightstreamer::Session#bulk_subscription_start`
8
+
3
9
  ### 0.8 — August 2, 2016
4
10
 
5
11
  - Added support for Lightstreamer's `COMMAND` and `RAW` subscription modes
6
12
  - Added support for Lightstreamer's polling mode and switching an active session between streaming and polling
7
- - `Lightstreamer::Subscription#item_data` now return `nil` if the requested item has never had any data set
13
+ - `Lightstreamer::Subscription#item_data` now returns `nil` if the requested item has never had any data set
8
14
  - Renamed `Lightstreamer::Subscription#adapter` to `Lightstreamer::Subscription#data_adapter` and the `--adapter`
9
15
  command-line option to `--data-adapter`
10
16
  - The `--mode` command-line option is now required because the default value of `merge` has been removed
data/README.md CHANGED
@@ -15,11 +15,11 @@ provided command-line client. Written against the
15
15
  Includes support for:
16
16
 
17
17
  - Streaming and polling connections
18
- - The four Lightstreamer subscription modes: `command`, `distinct`, `merge` and `raw`
19
- - Automatic management of table content when in `command` mode
18
+ - All subscription modes: command, distinct, merge and raw
19
+ - Automatic management of table content when in command mode
20
20
  - Silent subscriptions
21
- - Item snapshots
22
- - Unfiltered subscriptions and asynchronous overflow handling
21
+ - Item snapshots and end-of-snapshot notifications
22
+ - Unfiltered subscriptions and overflow notifications
23
23
  - Bulk subscription creation
24
24
  - Synchronous and asynchronous message sending
25
25
  - Detailed error reporting and error handling callbacks
@@ -43,23 +43,28 @@ The two primary classes that make up the public API are:
43
43
  - [`Lightstreamer::Session`](http://www.rubydoc.info/github/rviney/lightstreamer/Lightstreamer/Session)
44
44
  - [`Lightstreamer::Subscription`](http://www.rubydoc.info/github/rviney/lightstreamer/Lightstreamer/Subscription)
45
45
 
46
- The following code snippet demonstrates how to create a Lightstreamer session, build a subscription, then print
47
- streaming output as it arrives.
46
+ The following code demonstrates how to create a Lightstreamer session, build a subscription, then use a thread-safe
47
+ queue to print streaming output as it arrives.
48
48
 
49
49
  ```ruby
50
50
  require 'lightstreamer'
51
51
 
52
- # Create a new session that connects to the Lightstreamer demo server, which needs no authentication
52
+ # Create a new session that connects to the Lightstreamer demo server
53
53
  session = Lightstreamer::Session.new server_url: 'http://push.lightstreamer.com',
54
54
  adapter_set: 'DEMO', username: '', password: ''
55
55
 
56
+ # Add a simple error handler that just raises the error and so terminates the application
57
+ session.on_error do |error|
58
+ raise error
59
+ end
60
+
56
61
  # Connect the session
57
62
  session.connect
58
63
 
59
64
  # Create a new subscription that subscribes to thirty items and to four fields on each item
60
- subscription = session.build_subscription items: (1..30).map { |i| "item#{i}" },
65
+ subscription = session.build_subscription data_adapter: 'QUOTE_ADAPTER', mode: :merge,
66
+ items: (1..30).map { |i| "item#{i}" },
61
67
  fields: [:ask, :bid, :stock_name, :time],
62
- mode: :merge, data_adapter: 'QUOTE_ADAPTER'
63
68
 
64
69
  # Create a thread-safe queue
65
70
  queue = Queue.new
@@ -73,7 +78,7 @@ end
73
78
  # Start streaming data for the subscription and request an initial snapshot
74
79
  subscription.start snapshot: true
75
80
 
76
- # Loop printing out new data as soon as it becomes available on the queue
81
+ # Print new data as soon as it becomes available on the queue
77
82
  loop do
78
83
  data = queue.pop
79
84
  puts "#{data[:time]} - #{data[:stock_name]} - bid: #{data[:bid]}, ask: #{data[:ask]}"
@@ -82,10 +87,10 @@ end
82
87
 
83
88
  ## Usage — Command-Line Client
84
89
 
85
- This gem provides a simple command-line client that can connect to a Lightstreamer server, activate a
86
- subscription, then print streaming output from the server as it becomes available.
90
+ This gem provides a simple command-line client that can connect to a Lightstreamer server and display
91
+ live streaming output for a set of items and fields.
87
92
 
88
- To print streaming data from the demo server run the following command:
93
+ To stream data from Lightstreamer's demo server run the following command:
89
94
 
90
95
  ```
91
96
  lightstreamer --server-url http://push.lightstreamer.com --adapter-set DEMO \
@@ -101,7 +106,7 @@ lightstreamer help stream
101
106
 
102
107
  ## Documentation
103
108
 
104
- API documentation is available [here](http://www.rubydoc.info/github/rviney/lightstreamer).
109
+ API documentation is available [here](http://www.rubydoc.info/github/rviney/lightstreamer/master).
105
110
 
106
111
  ## Contributors
107
112
 
data/lib/lightstreamer.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'thread'
2
+
1
3
  require 'excon'
2
4
  require 'thor'
3
5
  require 'uri'
@@ -20,26 +20,36 @@ module Lightstreamer
20
20
  option :maximum_update_frequency, desc: 'The maximum number of updates per second for each item'
21
21
 
22
22
  def stream
23
- @queue = Queue.new
23
+ prepare_stream
24
24
 
25
- create_session
26
- create_subscription
25
+ puts "Session ID: #{@session.session_id}"
27
26
 
28
27
  loop do
29
- puts @queue.pop unless @queue.empty?
28
+ data = @queue.pop
30
29
 
31
- raise @session.error if @session.error
30
+ if data.is_a? Lightstreamer::LightstreamerError
31
+ puts "Error: #{data}"
32
+ break
33
+ end
34
+
35
+ puts data
32
36
  end
33
37
  end
34
38
 
35
39
  private
36
40
 
41
+ def prepare_stream
42
+ @queue = Queue.new
43
+
44
+ create_session
45
+ create_subscription
46
+ end
47
+
37
48
  def create_session
38
49
  @session = Lightstreamer::Session.new session_options
39
50
  @session.connect
40
51
  @session.on_message_result(&method(:on_message_result))
41
-
42
- puts "Session ID: #{@session.session_id}"
52
+ @session.on_error(&method(:on_error))
43
53
  end
44
54
 
45
55
  def create_subscription
@@ -79,6 +89,10 @@ module Lightstreamer
79
89
  def on_end_of_snapshot(_subscription, item_name)
80
90
  @queue.push "End of snapshot for item #{item_name}"
81
91
  end
92
+
93
+ def on_error(error)
94
+ @queue.push error
95
+ end
82
96
  end
83
97
  end
84
98
  end
@@ -25,13 +25,6 @@ module Lightstreamer
25
25
  # @return [String, nil]
26
26
  attr_reader :adapter_set
27
27
 
28
- # If an error occurs on the stream connection that causes the session to terminate then details of the error will be
29
- # stored in this attribute. If the session is terminated as a result of calling {#disconnect} then the error will be
30
- # {Errors::SessionEndError}.
31
- #
32
- # @return [LightstreamerError, nil]
33
- attr_reader :error
34
-
35
28
  # The server-side bandwidth constraint on data usage, expressed in kbps. If this is zero then no limit is applied.
36
29
  #
37
30
  # @return [Float]
@@ -58,8 +51,7 @@ module Lightstreamer
58
51
  # @option options [Boolean] :polling_enabled Whether polling mode is enabled. See {#polling_enabled} for details.
59
52
  # Defaults to `false`.
60
53
  def initialize(options = {})
61
- @subscriptions = []
62
- @subscriptions_mutex = Mutex.new
54
+ @mutex = Mutex.new
63
55
 
64
56
  @server_url = options.fetch :server_url
65
57
  @username = options[:username]
@@ -68,7 +60,8 @@ module Lightstreamer
68
60
  @requested_maximum_bandwidth = options[:requested_maximum_bandwidth].to_f
69
61
  @polling_enabled = options[:polling_enabled]
70
62
 
71
- @on_message_result_callbacks = []
63
+ @subscriptions = []
64
+ @callbacks = { on_message_result: [], on_error: [] }
72
65
  end
73
66
 
74
67
  # Connects a new Lightstreamer session using the details passed to {#initialize}. If an error occurs then
@@ -76,8 +69,6 @@ module Lightstreamer
76
69
  def connect
77
70
  return if @stream_connection
78
71
 
79
- @error = nil
80
-
81
72
  @stream_connection = StreamConnection.new self
82
73
  @stream_connection.connect
83
74
 
@@ -110,9 +101,7 @@ module Lightstreamer
110
101
  @stream_connection.disconnect if @stream_connection
111
102
  @processing_thread.exit if @processing_thread
112
103
 
113
- @subscriptions.each do |subscription|
114
- subscription.instance_variable_set :@active, false
115
- end
104
+ @subscriptions.each { |subscription| subscription.instance_variable_set :@active, false }
116
105
 
117
106
  @processing_thread = @stream_connection = nil
118
107
  end
@@ -124,9 +113,7 @@ module Lightstreamer
124
113
  # it forces the stream connection to rebind using the new setting. If an error occurs then a {LightstreamerError}
125
114
  # subclass will be raised.
126
115
  def force_rebind
127
- return unless @stream_connection
128
-
129
- control_request :force_rebind
116
+ control_request :force_rebind if @stream_connection
130
117
  end
131
118
 
132
119
  # Builds a new subscription for this session with the specified options. Note that ths does not activate the
@@ -149,7 +136,7 @@ module Lightstreamer
149
136
  def build_subscription(options)
150
137
  subscription = Subscription.new self, options
151
138
 
152
- @subscriptions_mutex.synchronize { @subscriptions << subscription }
139
+ @mutex.synchronize { @subscriptions << subscription }
153
140
 
154
141
  subscription
155
142
  end
@@ -162,7 +149,7 @@ module Lightstreamer
162
149
  def remove_subscription(subscription)
163
150
  subscription.stop
164
151
 
165
- @subscriptions_mutex.synchronize { @subscriptions.delete subscription }
152
+ @mutex.synchronize { @subscriptions.delete subscription }
166
153
  end
167
154
 
168
155
  # This method performs a bulk {Subscription#start} on all the passed subscriptions. Calling {Subscription#start} on
@@ -177,10 +164,11 @@ module Lightstreamer
177
164
  # @return [Array<LightstreamerError, nil>]
178
165
  def bulk_subscription_start(*subscriptions)
179
166
  request_bodies = subscriptions.map do |subscription|
180
- PostRequest.request_body session_id, *subscription.start_control_request_args
167
+ args = subscription.start_control_request_args
168
+ PostRequest.request_body({ LS_session: session_id, LS_op: args.first }.merge(args[1]))
181
169
  end
182
170
 
183
- errors = PostRequest.bulk_execute @stream_connection.control_address, request_bodies
171
+ errors = PostRequest.bulk_execute control_request_url, request_bodies
184
172
 
185
173
  # Set @active to true on all subscriptions that did not have an error
186
174
  errors.each_with_index do |error, index|
@@ -232,7 +220,7 @@ module Lightstreamer
232
220
  #
233
221
  # @param [Proc] callback The callback that is to be run.
234
222
  def on_message_result(&callback)
235
- @on_message_result_callbacks << callback
223
+ @mutex.synchronize { @callbacks[:on_message_result] << callback }
236
224
  end
237
225
 
238
226
  # Sends a request to this session's control connection. If an error occurs then a {LightstreamerError} subclass will
@@ -241,34 +229,49 @@ module Lightstreamer
241
229
  # @param [Symbol] operation The control operation to perform.
242
230
  # @param [Hash] options The options to send with the control request.
243
231
  def control_request(operation, options = {})
244
- url = URI.join(@stream_connection.control_address, '/lightstreamer/control.txt').to_s
232
+ PostRequest.execute control_request_url, options.merge(LS_session: session_id, LS_op: operation)
233
+ end
245
234
 
246
- PostRequest.execute url, options.merge(LS_session: session_id, LS_op: operation)
235
+ # Adds the passed block to the list of callbacks that will be run when this session encounters an error on its
236
+ # processing thread caused by an error with the steam connection. The block will be called on a worker thread and so
237
+ # the code that is run by the block must be thread-safe. The argument passed to the block is `|error|`, which will
238
+ # be a {LightstreamerError} subclass detailing the error that occurred.
239
+ #
240
+ # @param [Proc] callback The callback that is to be run.
241
+ def on_error(&callback)
242
+ @mutex.synchronize { @callbacks[:on_error] << callback }
247
243
  end
248
244
 
249
245
  private
250
246
 
247
+ def control_request_url
248
+ URI.join(@stream_connection.control_address, '/lightstreamer/control.txt').to_s
249
+ end
250
+
251
251
  # Starts the processing thread that reads and processes incoming data from the stream connection.
252
252
  def create_processing_thread
253
253
  @processing_thread = Thread.new do
254
254
  Thread.current.abort_on_exception = true
255
255
 
256
- loop do
257
- line = @stream_connection.read_line
258
- break if line.nil?
259
-
260
- process_stream_line line
261
- end
256
+ loop { break unless processing_thread_tick @stream_connection.read_line }
262
257
 
263
- # The stream connection has terminated so the session is assumed to be over
264
- @error = @stream_connection.error
265
258
  @processing_thread = @stream_connection = nil
266
259
  end
267
260
  end
268
261
 
262
+ def processing_thread_tick(line)
263
+ if line
264
+ process_stream_line line
265
+ true
266
+ else
267
+ @mutex.synchronize { @callbacks[:on_error].each { |callback| callback.call @stream_connection.error } }
268
+ false
269
+ end
270
+ end
271
+
269
272
  # Processes a single line of incoming stream data. This method is always run on the processing thread.
270
273
  def process_stream_line(line)
271
- return if @subscriptions.any? { |subscription| subscription.process_stream_data line }
274
+ return if @mutex.synchronize { @subscriptions.any? { |subscription| subscription.process_stream_data line } }
272
275
  return if process_send_message_outcome line
273
276
 
274
277
  warn "Lightstreamer: unprocessed stream data '#{line}'"
@@ -279,8 +282,10 @@ module Lightstreamer
279
282
  outcome = SendMessageOutcomeMessage.parse line
280
283
  return unless outcome
281
284
 
282
- @on_message_result_callbacks.each do |callback|
283
- callback.call outcome.sequence, outcome.numbers, outcome.error
285
+ @mutex.synchronize do
286
+ @callbacks[:on_message_result].each do |callback|
287
+ callback.call outcome.sequence, outcome.numbers, outcome.error
288
+ end
284
289
  end
285
290
 
286
291
  true
@@ -172,7 +172,7 @@ module Lightstreamer
172
172
  def process_body_line(line)
173
173
  if line =~ /^LOOP( \d+|)$/
174
174
  @loop = true
175
- elsif line =~ /^END( \d+|)/
175
+ elsif line =~ /^END( \d+|)$/
176
176
  @error = Errors::SessionEndError.new line[4..-1]
177
177
  elsif !line.empty?
178
178
  @queue.push line
@@ -56,8 +56,9 @@ module Lightstreamer
56
56
  #
57
57
  # @private
58
58
  def initialize(session, options)
59
- @session = session
59
+ @mutex = Mutex.new
60
60
 
61
+ @session = session
61
62
  @items = options.fetch(:items)
62
63
  @fields = options.fetch(:fields)
63
64
  @mode = options.fetch(:mode).to_sym
@@ -65,8 +66,6 @@ module Lightstreamer
65
66
  @selector = options[:selector]
66
67
  @maximum_update_frequency = sanitize_frequency options[:maximum_update_frequency]
67
68
 
68
- @data_mutex = Mutex.new
69
-
70
69
  clear_data
71
70
  clear_callbacks
72
71
  end
@@ -148,7 +147,7 @@ module Lightstreamer
148
147
  # Clears all current data stored for this subscription. New data will continue to be processed as it becomes
149
148
  # available.
150
149
  def clear_data
151
- @data_mutex.synchronize { @data = (0...items.size).map { SubscriptionItemData.new } }
150
+ @mutex.synchronize { @data = (0...items.size).map { SubscriptionItemData.new } }
152
151
  end
153
152
 
154
153
  # Returns a copy of the current data of one of this subscription's items. If {#mode} is `:merge` then the returned
@@ -163,7 +162,7 @@ module Lightstreamer
163
162
  index = @items.index item_name
164
163
  raise ArgumentError, 'Unknown item' unless index
165
164
 
166
- @data_mutex.synchronize { @data[index].data && @data[index].data.dup }
165
+ @mutex.synchronize { @data[index].data && @data[index].data.dup }
167
166
  end
168
167
 
169
168
  # Sets the current data for the item with the specified name. This is only allowed when {mode} is `:command` or
@@ -177,7 +176,7 @@ module Lightstreamer
177
176
  index = @items.index item_name
178
177
  raise ArgumentError, 'Unknown item' unless index
179
178
 
180
- @data_mutex.synchronize { @data[index].set_data item_data, mode }
179
+ @mutex.synchronize { @data[index].set_data item_data, mode }
181
180
  end
182
181
 
183
182
  # Adds the passed block to the list of callbacks that will be run when new data for this subscription arrives. The
@@ -187,7 +186,7 @@ module Lightstreamer
187
186
  #
188
187
  # @param [Proc] callback The callback that is to be run when new data arrives.
189
188
  def on_data(&callback)
190
- @data_mutex.synchronize { @callbacks[:on_data] << callback }
189
+ @mutex.synchronize { @callbacks[:on_data] << callback }
191
190
  end
192
191
 
193
192
  # Adds the passed block to the list of callbacks that will be run when the server reports an overflow for this
@@ -198,7 +197,7 @@ module Lightstreamer
198
197
  #
199
198
  # @param [Proc] callback The callback that is to be run when an overflow is reported for this subscription.
200
199
  def on_overflow(&callback)
201
- @data_mutex.synchronize { @callbacks[:on_overflow] << callback }
200
+ @mutex.synchronize { @callbacks[:on_overflow] << callback }
202
201
  end
203
202
 
204
203
  # Adds the passed block to the list of callbacks that will be run when the server reports an end-of-snapshot
@@ -209,12 +208,12 @@ module Lightstreamer
209
208
  #
210
209
  # @param [Proc] callback The callback that is to be run when an overflow is reported for this subscription.
211
210
  def on_end_of_snapshot(&callback)
212
- @data_mutex.synchronize { @callbacks[:on_end_of_snapshot] << callback }
211
+ @mutex.synchronize { @callbacks[:on_end_of_snapshot] << callback }
213
212
  end
214
213
 
215
214
  # Removes all {#on_data}, {#on_overflow} and {#on_end_of_snapshot} callbacks present on this subscription.
216
215
  def clear_callbacks
217
- @data_mutex.synchronize { @callbacks = { on_data: [], on_overflow: [], on_end_of_snapshot: [] } }
216
+ @mutex.synchronize { @callbacks = { on_data: [], on_overflow: [], on_end_of_snapshot: [] } }
218
217
  end
219
218
 
220
219
  # Processes a line of stream data if it is relevant to this subscription. This method is thread-safe and is intended
@@ -242,7 +241,7 @@ module Lightstreamer
242
241
  def process_update_message(message)
243
242
  return unless message
244
243
 
245
- @data_mutex.synchronize do
244
+ @mutex.synchronize do
246
245
  @data[message.item_index].send "process_new_#{mode}_data", message.data.dup
247
246
  run_callbacks :on_data, @items[message.item_index], @data[message.item_index].data, message.data
248
247
  end
@@ -251,13 +250,13 @@ module Lightstreamer
251
250
  def process_overflow_message(message)
252
251
  return unless message
253
252
 
254
- @data_mutex.synchronize { run_callbacks :on_overflow, @items[message.item_index], message.overflow_size }
253
+ @mutex.synchronize { run_callbacks :on_overflow, @items[message.item_index], message.overflow_size }
255
254
  end
256
255
 
257
256
  def process_end_of_snapshot_message(message)
258
257
  return unless message
259
258
 
260
- @data_mutex.synchronize { run_callbacks :on_end_of_snapshot, @items[message.item_index] }
259
+ @mutex.synchronize { run_callbacks :on_end_of_snapshot, @items[message.item_index] }
261
260
  end
262
261
 
263
262
  def run_callbacks(callback_type, *args)
@@ -1,4 +1,4 @@
1
1
  module Lightstreamer
2
2
  # The version of this gem.
3
- VERSION = '0.8'.freeze
3
+ VERSION = '0.9'.freeze
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lightstreamer
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.8'
4
+ version: '0.9'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Viney
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-08-02 00:00:00.000000000 Z
11
+ date: 2016-08-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: excon