lightstreamer 0.8 → 0.9

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: 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