lightstreamer 0.3 → 0.4

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: f96a70c42b710644985ba440779ec2d73f32ad9e
4
- data.tar.gz: 4c0181a0612a67870d3333671ecbb20e0af27f8c
3
+ metadata.gz: 863ad0ba9936b00dca802004782358a03cf5cd36
4
+ data.tar.gz: a13adc3e4b6b9945580bc02927826d61742dba1d
5
5
  SHA512:
6
- metadata.gz: 5048265eaf80652222323ffad4f4fa4a39343c4adbc8a52ae5b449d43a8c63e343334cab654a2f6fdc3321955b418581661e0c596e23f1f3ea83f953e9a77f02
7
- data.tar.gz: 248b20766df771c73189fd3eb61f218a5b016254a2271f965c4076cfde2bd9391eef4ab6afcc1ea5a5424a3ca86c4f15728aeb12bf415bb2b03721722828bced
6
+ metadata.gz: 7f9dd91cc66467027ddd209d0c964559db461e8b9a05c1cb9aa583ae42d1997516c0196ea2be18c0fbc5d317db97fcef10ce81e32cefe677390dd4c8f339ebfc
7
+ data.tar.gz: 4863842643a0d7a876f6efa2b41270044f7ada1479c0154e452e02f7e1d6b2e75c39f6f24d79029b633dd7402c3cd6a5d24a41e5c3dba057013a0ddc403b9dbc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Lightstreamer Changelog
2
2
 
3
+ ### 0.4 — July 25, 2016
4
+
5
+ - Added support for specifying a subscription's selector and maximum update frequency
6
+ - Added `Lightstreamer::Session#force_rebind` which asks the server to send a `LOOP` message so the client has to rebind
7
+ using a new stream connection
8
+ - Added validation of the arguments for control connection subscription requests
9
+ - All error classes now inherit from `Lightstreamer::Error` and the different Lightstreamer errors that can occur are
10
+ separated into a variety of new subclasses
11
+ - Removed `Lightstreamer::ProtocolError`
12
+ - `Lightstreamer::Session#disconnect` now properly terminates the session with the server by sending the relevant
13
+ control request
14
+ - The reason why a session terminated can now be queried using `Lightstreamer::Session#error`
15
+ - Fixed handling of `nil` subscription adapters
16
+ - Correctly handle when an `END` message is received on the stream connection
17
+ - All valid error responses from session create and bind requests are now handled correctly
18
+ - Unhandled exceptions on the internal worker threads now cause the application to terminate
19
+
3
20
  ### 0.3 — July 24, 2016
4
21
 
5
22
  - Seamlessly rebind the stream connection when a `LOOP` message is received
data/lib/lightstreamer.rb CHANGED
@@ -3,9 +3,8 @@ require 'typhoeus'
3
3
  require 'uri'
4
4
 
5
5
  require 'lightstreamer/control_connection'
6
+ require 'lightstreamer/errors'
6
7
  require 'lightstreamer/line_buffer'
7
- require 'lightstreamer/protocol_error'
8
- require 'lightstreamer/request_error'
9
8
  require 'lightstreamer/session'
10
9
  require 'lightstreamer/stream_connection'
11
10
  require 'lightstreamer/stream_connection_header'
@@ -12,6 +12,8 @@ module Lightstreamer
12
12
  option :items, type: :array, required: true, desc: 'The names of the item(s) to stream'
13
13
  option :fields, type: :array, required: true, desc: 'The field(s) to stream'
14
14
  option :mode, enum: %w(distinct merge), default: :merge, desc: 'The operation mode'
15
+ option :selector, desc: 'The selector for table items'
16
+ option :maximum_update_frequency, type: :numeric, desc: 'The maximum number of updates per second for each item'
15
17
 
16
18
  def stream
17
19
  session = create_session
@@ -36,14 +38,20 @@ module Lightstreamer
36
38
 
37
39
  # Creates a new subscription from the specified options.
38
40
  def create_subscription
39
- subscription = Lightstreamer::Subscription.new items: options[:items], fields: options[:fields],
40
- mode: options[:mode], adapter: options[:adapter]
41
+ subscription = Lightstreamer::Subscription.new subscription_options
41
42
 
42
43
  subscription.add_data_callback(&method(:subscription_data_callback))
43
44
 
44
45
  subscription
45
46
  end
46
47
 
48
+ def subscription_options
49
+ {
50
+ items: options[:items], fields: options[:fields], mode: options[:mode], adapter: options[:adapter],
51
+ maximum_update_frequency: options[:maximum_update_frequency], selector: options[:selector]
52
+ }
53
+ end
54
+
47
55
  def subscription_data_callback(_subscription, item_name, _item_data, new_values)
48
56
  @queue.push "#{item_name} - #{new_values.map { |key, value| "#{key}: #{value}" }.join ', '}"
49
57
  end
@@ -11,26 +11,56 @@ module Lightstreamer
11
11
  @control_url = URI.join(control_url, '/lightstreamer/control.txt').to_s
12
12
  end
13
13
 
14
- # Sends a Lightstreamer control request with the specified options. If an error occurs then {RequestError} or
15
- # {ProtocolError} will be raised.
14
+ # Sends a Lightstreamer control request that executes the specified operation with the specified options. If an
15
+ # error occurs then an {Error} subclass will be raised.
16
16
  #
17
- # @param [Hash] options The control request options.
18
- # @option options [Fixnum] :table The ID of the table this request pertains to. Required.
19
- # @option options [:add, :add_silent, :start, :delete] :operation The operation to perform. Required.
17
+ # @param [String] operation The operation to execute.
18
+ # @param [Hash] options The options to include on the request.
19
+ def execute(operation, options = {})
20
+ result = execute_post_request build_payload(operation, options)
21
+
22
+ raise Error.build(result[2], result[1]) if result.first != 'OK'
23
+ end
24
+
25
+ # Sends a Lightstreamer subscription control request with the specified operation, table, and options. If an error
26
+ # occurs then an {Error} subclass will be raised.
27
+ #
28
+ # @param [:add, :add_silent, :start, :delete] operation The operation to execute.
29
+ # @param [Fixnum] table The ID of the table this request pertains to.
30
+ # @param [Hash] options The subscription control request options.
20
31
  # @option options [String] :adapter The name of the data adapter to use. Optional.
21
32
  # @option options [Array<String>] :items The names of the items that this request pertains to. Required if
22
- # `:operation` is `:add` or `:add_silent`.
33
+ # `operation` is `:add` or `:add_silent`.
23
34
  # @option options [Array<String>] :fields The names of the fields that this request pertains to. Required if
24
- # `:operation` is `:add` or `:add_silent`.
25
- # @option options [:raw, :merge, :distinct, :command] :mode The subscription mode.
26
- def execute(options)
27
- result = execute_post_request build_payload(options)
35
+ # `operation` is `:add` or `:add_silent`.
36
+ # @option options [:distinct, :merge] :mode The subscription mode. Required if `operation` is `:add` or
37
+ # `:add_silent`.
38
+ # @option options [String] :selector The selector for table items. Optional.
39
+ def subscription_execute(operation, table, options = {})
40
+ options[:table] = table
28
41
 
29
- raise ProtocolError.new(result[2], result[1]) if result.first == 'ERROR'
42
+ validate_subscription_options operation, options
43
+
44
+ execute operation, options
30
45
  end
31
46
 
32
47
  private
33
48
 
49
+ # Validates the passed subscription control request options.
50
+ def validate_subscription_options(operation, options)
51
+ raise ArgumentError, 'Invalid table' unless options[:table].is_a? Fixnum
52
+ raise ArgumentError, 'Unsupported operation' unless [:add, :add_silent, :start, :delete].include? operation
53
+
54
+ validate_add_subscription_options options if [:add, :add_silent].include? operation
55
+ end
56
+
57
+ # Validates options required for subscription control requests that perform `add` operations.
58
+ def validate_add_subscription_options(options)
59
+ raise ArgumentError, 'Items not specified' if Array(options[:items]).empty?
60
+ raise ArgumentError, 'Fields not specified' if Array(options[:fields]).empty?
61
+ raise ArgumentError, 'Unsupported mode' unless [:distinct, :merge].include? options[:mode]
62
+ end
63
+
34
64
  # Executes a POST request to the control address with the specified payload. Raises {RequestError} if the HTTP
35
65
  # request fails. Returns the response body split into individual lines.
36
66
  def execute_post_request(payload)
@@ -43,23 +73,39 @@ module Lightstreamer
43
73
 
44
74
  # Constructs the payload for a Lightstreamer control request based on the given options hash. See {#execute} for
45
75
  # details on the supported keys.
46
- def build_payload(options)
47
- params = {
48
- LS_session: @session_id,
49
- LS_table: options.fetch(:table),
50
- LS_op: options.fetch(:operation)
51
- }
76
+ def build_payload(operation, options)
77
+ params = {}
52
78
 
53
79
  build_optional_payload_fields options, params
54
80
 
81
+ params[:LS_session] = @session_id
82
+ params[:LS_op] = operation
83
+
55
84
  params
56
85
  end
57
86
 
87
+ OPTION_NAME_TO_API_PARAMETER = {
88
+ table: :LS_table,
89
+ adapter: :LS_data_adapter,
90
+ items: :LS_id,
91
+ fields: :LS_schema,
92
+ selector: :LS_selector,
93
+ maximum_update_frequency: :LS_requested_max_frequency
94
+ }.freeze
95
+
58
96
  def build_optional_payload_fields(options, params)
59
- params[:LS_data_adapter] = options[:adapter] if options.key? :adapter
60
- params[:LS_id] = options[:items].join(' ') if options.key? :items
61
- params[:LS_schema] = options[:fields].map(&:to_s).join(' ') if options.key? :fields
62
- params[:LS_mode] = options[:mode].to_s.upcase if options.key? :mode
97
+ params[:LS_mode] = options[:mode].to_s.upcase if options[:mode]
98
+
99
+ options.each do |key, value|
100
+ next if key == :mode
101
+ next if value.nil?
102
+
103
+ value = value.map(&:to_s).join(' ') if value.is_a? Array
104
+
105
+ api_parameter = OPTION_NAME_TO_API_PARAMETER.fetch key
106
+
107
+ params[api_parameter] = value
108
+ end
63
109
  end
64
110
  end
65
111
  end
@@ -0,0 +1,206 @@
1
+ module Lightstreamer
2
+ # Base class for all errors raised by this gem.
3
+ class Error < StandardError
4
+ end
5
+
6
+ # This error is raised when the session username and password check fails.
7
+ class AuthenticationError < Error
8
+ end
9
+
10
+ # This error is raised when the requested adapter set is unknown.
11
+ class UnknownAdapterSetError < Error
12
+ end
13
+
14
+ # This error is raise when trying to bind to a session that was initialized with a different and incompatible
15
+ # communication protocol.
16
+ class IncompatibleSessionError < Error
17
+ end
18
+
19
+ # This error is raised when the licensed maximum number of sessions is reached.
20
+ class LicensedMaximumSessionsReachedError < Error
21
+ end
22
+
23
+ # This error is raised when the configured maximum number of sessions is reached.
24
+ class ConfiguredMaximumSessionsReachedError < Error
25
+ end
26
+
27
+ # This error is raised when the configured maximum server load is reached.
28
+ class ConfiguredMaximumServerLoadReachedError < Error
29
+ end
30
+
31
+ # This error is raised when the creation of new sessions has been temporarily blocked.
32
+ class NewSessionsTemporarilyBlockedError < Error
33
+ end
34
+
35
+ # This error is raised when streaming is not available because of the current license terms.
36
+ class StreamingNotAvailableError < Error
37
+ end
38
+
39
+ # This error is raised when the specified table can't be modified because it is configured for unfiltered dispatching.
40
+ class TableModificationNotAllowedError < Error
41
+ end
42
+
43
+ # This error is raised when the specified data adapter is invalid or the data adapter is not specified and there is
44
+ # no default data adapter.
45
+ class InvalidDataAdapterError < Error
46
+ end
47
+
48
+ # This error occurs when the specified table is not found.
49
+ class UnknownTableError < Error
50
+ end
51
+
52
+ # This error is raised when an invalid item name is specified.
53
+ class InvalidItemError < Error
54
+ end
55
+
56
+ # This error is raised when an invalid item name for the given fields is specified.
57
+ class InvalidItemForFieldsError < Error
58
+ end
59
+
60
+ # This error is raised when an invalid field name is specified.
61
+ class InvalidFieldError < Error
62
+ end
63
+
64
+ # This error is raised when the specified subscription mode is not supported by one of the items.
65
+ class UnsupportedModeForItemError < Error
66
+ end
67
+
68
+ # This error is raised when an invalid selector is specified.
69
+ class InvalidSelectorError < Error
70
+ end
71
+
72
+ # This error is raised when unfiltered dispatching is requested on an item that does not allow it.
73
+ class UnfilteredDispatchingNotAllowedForItemError < Error
74
+ end
75
+
76
+ # This error is raised when unfiltered dispatching is requested on an item that does not support it.
77
+ class UnfilteredDispatchingNotSupportedForItemError < Error
78
+ end
79
+
80
+ # This error is raised when unfiltered dispatching is requested but is not allowed by the current license terms.
81
+ class UnfilteredDispatchingNotAllowedByLicenseError < Error
82
+ end
83
+
84
+ # This error is raised when `RAW` mode was requested but is not allowed by the current license terms.
85
+ class RawModeNotAllowedByLicenseError < Error
86
+ end
87
+
88
+ # This error is raised when subscriptions are not allowed by the current license terms.
89
+ class SubscriptionsNotAllowedByLicenseError < Error
90
+ end
91
+
92
+ # This error is raised when the specified progressive sequence number for the custom message was invalid.
93
+ class InvalidProgressiveNumberError < Error
94
+ end
95
+
96
+ # This error is raised when the client version requested is not supported by the server.
97
+ class ClientVersionNotSupportedError < Error
98
+ end
99
+
100
+ # This error is raised when a error defined by a metadata adapter is raised.
101
+ class MetadataAdapterError < Error
102
+ # @return [String] The error message from the metadata adapter.
103
+ attr_reader :adapter_error_message
104
+
105
+ # @return [Fixnum] The error code from the metadata adapter.
106
+ attr_reader :adapter_error_code
107
+
108
+ # Initializes this metadata adapter error with the specified error message and error code.
109
+ def initialize(message, code)
110
+ @adapter_error_message = message
111
+ @adapter_error_code = code.to_i
112
+
113
+ super message
114
+ end
115
+ end
116
+
117
+ # This error is raised when a sync error occurs, which most often means that the session ID provided is invalid and
118
+ # a new session needs to be created.
119
+ class SyncError < Error
120
+ end
121
+
122
+ # This error is raised when the specified session ID is for a session that has been terminated.
123
+ class SessionEndError < Error
124
+ # @return [Fixnum] The cause code specifying why the session was terminated by the server, or `nil` if unknown.
125
+ attr_reader :cause_code
126
+
127
+ # Initializes this session end error with the specified cause code.
128
+ #
129
+ # @param [Fixnum] cause_code
130
+ def initialize(cause_code = nil)
131
+ @cause_code = cause_code.to_i
132
+ super()
133
+ end
134
+ end
135
+
136
+ # This error is raised when an HTTP request error occurs.
137
+ class RequestError < Error
138
+ # @return [String] A description of the request error that occurred.
139
+ attr_reader :request_error_message
140
+
141
+ # @return [Fixnum] The HTTP code that was returned, or zero if unknown.
142
+ attr_reader :request_error_code
143
+
144
+ # Initializes this request error with a message and an HTTP code.
145
+ #
146
+ # @param [String] message The error description.
147
+ # @param [Integer] code The HTTP code for the request failure, if known.
148
+ def initialize(message, code)
149
+ @request_error_message = message
150
+ @request_error_code = code.to_i
151
+
152
+ super "Request error #{code}: #{message}"
153
+ end
154
+ end
155
+
156
+ # Base class for all errors raised by this gem.
157
+ class Error
158
+ # Takes a Lightstreamer error message and numeric code and returns an instance of the relevant error class that
159
+ # should be raised in response to the error.
160
+ #
161
+ # @param [String] message The error message.
162
+ # @param [Fixnum] code The numeric error code that is used to determine which {Error} subclass to instantiate.
163
+ #
164
+ # @return [Error]
165
+ def self.build(message, code)
166
+ code = code.to_i
167
+
168
+ if API_ERROR_CODE_TO_CLASS.key? code
169
+ API_ERROR_CODE_TO_CLASS[code].new
170
+ elsif code <= 0
171
+ MetadataAdapterError.new message, code
172
+ else
173
+ new message
174
+ end
175
+ end
176
+
177
+ API_ERROR_CODE_TO_CLASS = {
178
+ 1 => AuthenticationError,
179
+ 2 => UnknownAdapterSetError,
180
+ 3 => IncompatibleSessionError,
181
+ 7 => LicensedMaximumSessionsReachedError,
182
+ 8 => ConfiguredMaximumSessionsReachedError,
183
+ 9 => ConfiguredMaximumServerLoadReachedError,
184
+ 10 => NewSessionsTemporarilyBlockedError,
185
+ 11 => StreamingNotAvailableError,
186
+ 13 => TableModificationNotAllowedError,
187
+ 17 => InvalidDataAdapterError,
188
+ 19 => UnknownTableError,
189
+ 21 => InvalidItemError,
190
+ 22 => InvalidItemForFieldsError,
191
+ 23 => InvalidFieldError,
192
+ 24 => UnsupportedModeForItemError,
193
+ 25 => InvalidSelectorError,
194
+ 26 => UnfilteredDispatchingNotAllowedForItemError,
195
+ 27 => UnfilteredDispatchingNotSupportedForItemError,
196
+ 28 => UnfilteredDispatchingNotAllowedByLicenseError,
197
+ 29 => RawModeNotAllowedByLicenseError,
198
+ 30 => SubscriptionsNotAllowedByLicenseError,
199
+ 32 => InvalidProgressiveNumberError,
200
+ 33 => InvalidProgressiveNumberError,
201
+ 60 => ClientVersionNotSupportedError
202
+ }.freeze
203
+
204
+ private_constant :API_ERROR_CODE_TO_CLASS
205
+ end
206
+ end
@@ -14,6 +14,11 @@ module Lightstreamer
14
14
  # @return [String] The name of the adapter set to request from the Lightstreamer server. Set by {#initialize}.
15
15
  attr_reader :adapter_set
16
16
 
17
+ # @return [Error] If an error occurs on the stream connection that causes the session to terminate then details of
18
+ # the error will be stored in this attribute. If the session is terminated as a result of calling
19
+ # {#disconnect} then the error will be {SessionEndError}.
20
+ attr_reader :error
21
+
17
22
  # Initializes this new Lightstreamer session with the passed options.
18
23
  #
19
24
  # @param [Hash] options The options to create the session with.
@@ -32,10 +37,12 @@ module Lightstreamer
32
37
  end
33
38
 
34
39
  # Creates a new Lightstreamer session using the details passed to {#initialize}. If an error occurs then
35
- # {ProtocolError} will be raised.
40
+ # an {Error} subclass will be raised.
36
41
  def connect
37
42
  return if @stream_connection
38
43
 
44
+ @error = nil
45
+
39
46
  create_stream_connection
40
47
  create_control_connection
41
48
  create_processing_thread
@@ -51,20 +58,31 @@ module Lightstreamer
51
58
  !@stream_connection.nil?
52
59
  end
53
60
 
54
- # Disconnects this session and shuts down its stream connection and processing threads.
61
+ # Disconnects this session and terminates the session on the server. All worker threads are exited.
55
62
  def disconnect
56
- @stream_connection.disconnect if @stream_connection
63
+ @control_connection.execute :destroy if @control_connection
57
64
 
58
- if @processing_thread
59
- Thread.kill @processing_thread
60
- @processing_thread.join
61
- end
65
+ @processing_thread.join 5 if @processing_thread
66
+ ensure
67
+ @stream_connection.disconnect if @stream_connection
68
+ @processing_thread.exit if @processing_thread
62
69
 
63
- @processing_thread = @stream_connection = @control_connection = nil
70
+ @processing_thread = @control_connection = @stream_connection = nil
64
71
  @subscriptions = []
65
72
  end
66
73
 
67
- # Subscribes this Lightstreamer session to the specified subscription.
74
+ # Requests that the Lightstreamer server terminate the currently active stream connection and require that a new
75
+ # stream connection be initiated by the client. The Lightstreamer server requires closure and re-establishment of
76
+ # the stream connection periodically during normal operation, this method just allows such a reconnection to be
77
+ # requested explicitly by the client. If an error occurs then an {Error} subclass will be raised.
78
+ def force_rebind
79
+ return unless @stream_connection
80
+
81
+ @control_connection.execute :force_rebind
82
+ end
83
+
84
+ # Subscribes this Lightstreamer session to the specified subscription. If an error occurs then an {Error} subclass
85
+ # will be raised.
68
86
  #
69
87
  # @param [Subscription] subscription The new subscription to subscribe to.
70
88
  def subscribe(subscription)
@@ -72,14 +90,14 @@ module Lightstreamer
72
90
 
73
91
  @subscriptions_mutex.synchronize { @subscriptions << subscription }
74
92
 
75
- begin
76
- @control_connection.execute table: subscription.id, operation: :add, mode: subscription.mode,
77
- items: subscription.items, fields: subscription.fields,
78
- adapter: subscription.adapter
79
- rescue
80
- @subscriptions_mutex.synchronize { @subscriptions.delete subscription }
81
- raise
82
- end
93
+ options = { mode: subscription.mode, items: subscription.items, fields: subscription.fields,
94
+ adapter: subscription.adapter, maximum_update_frequency: subscription.maximum_update_frequency,
95
+ selector: subscription.selector }
96
+
97
+ @control_connection.subscription_execute :add, subscription.id, options
98
+ rescue
99
+ @subscriptions_mutex.synchronize { @subscriptions.delete subscription }
100
+ raise
83
101
  end
84
102
 
85
103
  # Returns whether the specified subscription is currently active on this session.
@@ -97,7 +115,7 @@ module Lightstreamer
97
115
  def unsubscribe(subscription)
98
116
  raise ArgumentError, 'Unknown subscription' unless subscribed? subscription
99
117
 
100
- @control_connection.execute table: subscription.id, operation: :delete
118
+ @control_connection.subscription_execute :delete, subscription.id
101
119
 
102
120
  @subscriptions_mutex.synchronize { @subscriptions.delete subscription }
103
121
  end
@@ -123,6 +141,8 @@ module Lightstreamer
123
141
  # Starts the processing thread that reads and processes incoming data from the stream connection.
124
142
  def create_processing_thread
125
143
  @processing_thread = Thread.new do
144
+ Thread.current.abort_on_exception = true
145
+
126
146
  loop do
127
147
  line = @stream_connection.read_line
128
148
 
@@ -131,9 +151,9 @@ module Lightstreamer
131
151
  process_stream_data line unless line.empty?
132
152
  end
133
153
 
134
- # The stream connection has died, so exit the processing thread
135
- warn "Lightstreamer: processing thread exiting, error: #{@stream_connection.error}"
136
- @processing_thread = @stream_connection = @control_connection = nil
154
+ # The stream connection has terminated so the session is assumed to be over
155
+ @error = @stream_connection.error
156
+ @processing_thread = @control_connection = @stream_connection = nil
137
157
  end
138
158
  end
139
159
 
@@ -11,8 +11,8 @@ module Lightstreamer
11
11
  # @return [String] The control address returned from the server when this stream connection was initiated.
12
12
  attr_reader :control_address
13
13
 
14
- # @return [ProtocolError, RequestError, String] If an error occurs on the stream thread that causes this stream
15
- # to disconnect then the exception or error details will be stored in this attribute.
14
+ # @return [Error] If an error occurs on the stream thread that causes the stream to disconnect then the
15
+ # error will be stored in this attribute.
16
16
  attr_reader :error
17
17
 
18
18
  # Establishes a new stream connection using the authentication details from the passed session.
@@ -27,10 +27,11 @@ module Lightstreamer
27
27
  end
28
28
 
29
29
  # Establishes a new stream connection using the authentication details from the session that was passed to
30
- # {#initialize}. Raises {ProtocolError} or {RequestError} on failure.
30
+ # {#initialize}. Raises an {Error} subclass on failure.
31
31
  def connect
32
32
  return if @thread
33
33
  @session_id = @error = nil
34
+ @queue.clear
34
35
 
35
36
  create_stream_thread
36
37
 
@@ -51,7 +52,7 @@ module Lightstreamer
51
52
  # Disconnects this stream connection by shutting down the streaming thread.
52
53
  def disconnect
53
54
  if @thread
54
- Thread.kill @thread
55
+ @thread.exit
55
56
  @thread.join
56
57
  end
57
58
 
@@ -73,22 +74,17 @@ module Lightstreamer
73
74
 
74
75
  def create_stream_thread
75
76
  @thread = Thread.new do
76
- begin
77
- stream_thread_main
78
- rescue StandardError => error
79
- @error = error
80
- end
77
+ Thread.current.abort_on_exception = true
81
78
 
82
- @thread = nil
83
- end
84
- end
79
+ connect_stream_and_process_data stream_create_post_request
85
80
 
86
- def stream_thread_main
87
- connect_stream_and_process_data stream_create_post_request
81
+ while @loop
82
+ @loop = false
83
+ connect_stream_and_process_data stream_bind_post_request
84
+ end
88
85
 
89
- while @loop
90
- @loop = false
91
- connect_stream_and_process_data stream_bind_post_request
86
+ @thread = nil
87
+ @queue.push nil
92
88
  end
93
89
  end
94
90
 
@@ -114,6 +110,7 @@ module Lightstreamer
114
110
  end
115
111
 
116
112
  request.on_complete do |response|
113
+ @error = @header.error if @header
117
114
  @error = RequestError.new(response.return_message, response.response_code) unless response.success?
118
115
  end
119
116
 
@@ -133,7 +130,6 @@ module Lightstreamer
133
130
 
134
131
  @control_address = @header['ControlAddress']
135
132
  @session_id = @header['SessionId']
136
- @error = @header.error
137
133
 
138
134
  @header = nil
139
135
  end
@@ -141,6 +137,8 @@ module Lightstreamer
141
137
  def process_body_line(line)
142
138
  if line == 'LOOP'
143
139
  @loop = true
140
+ elsif line =~ /^END/
141
+ @error = SessionEndError.new line[4..-1]
144
142
  elsif !ignore_line?(line)
145
143
  @queue.push line
146
144
  end
@@ -2,8 +2,8 @@ module Lightstreamer
2
2
  # Helper class that processes the contents of the header returned by the server when a new stream connection is
3
3
  # created or an existing session is bound to.
4
4
  class StreamConnectionHeader
5
- # @return [ProtocolError, RequestError] If there was an error in the header then this value will be set to the
6
- # error instance that should be raised in response.
5
+ # @return [Error] If there was an error in the header then this value will be set to the error instance that should
6
+ # be raised in response.
7
7
  attr_reader :error
8
8
 
9
9
  def initialize
@@ -20,21 +20,16 @@ module Lightstreamer
20
20
  def process_header_line(line)
21
21
  @lines << line
22
22
 
23
- unless %w(OK ERROR).include? @lines.first
24
- @error = RequestError.new line
25
- return false
26
- end
23
+ return process_success if @lines.first == 'OK'
24
+ return process_error if @lines.first == 'ERROR'
25
+ return process_end if @lines.first == 'END'
26
+ return process_sync_error if @lines.first == 'SYNC ERROR'
27
27
 
28
- return true if @lines.first == 'OK' && !line.empty?
29
- return true if @lines.first == 'ERROR' && @lines.size < 3
30
-
31
- parse_header
32
-
33
- false
28
+ process_unrecognized
34
29
  end
35
30
 
36
31
  # Returns the value for the item with the specified name in this header, or `nil` if no item with the specified name
37
- # was part of this header
32
+ # was part of this header.
38
33
  #
39
34
  # @param [String] item_name The name of the item to return the header value for.
40
35
  #
@@ -46,15 +41,31 @@ module Lightstreamer
46
41
 
47
42
  private
48
43
 
49
- def parse_header
50
- if @lines.first == 'OK'
51
- @lines[1..-1].each do |line|
52
- match = line.match(/^([^:]*):(.*)$/)
53
- @data[match.captures[0]] = match.captures[1] if match
54
- end
55
- elsif @lines.first == 'ERROR'
56
- @error = ProtocolError.new @lines[2], @lines[1]
57
- end
44
+ def process_success
45
+ match = @lines.last.match(/^([^:]*):(.*)$/)
46
+ @data[match.captures[0]] = match.captures[1] if match
47
+
48
+ !@lines.last.empty?
49
+ end
50
+
51
+ def process_error
52
+ @error = Error.build @lines[2], @lines[1]
53
+ true
54
+ end
55
+
56
+ def process_end
57
+ @error = SessionEndError.new @lines[1]
58
+ true
59
+ end
60
+
61
+ def process_sync_error
62
+ @error = SyncError.new
63
+ false
64
+ end
65
+
66
+ def process_unrecognized
67
+ @error = Error.new @lines.join(' ')
68
+ true
58
69
  end
59
70
  end
60
71
  end
@@ -1,8 +1,8 @@
1
1
  module Lightstreamer
2
- # Describes a subscription that can be bound to a Lightstreamer session in order to consume its data. A subscription
3
- # is described by the options passed to {#initialize}. Incoming data can be consumed by registering an asynchronous
4
- # data callback using {#add_data_callback}, or by polling {#retrieve_item_data}. Subscriptions start receiving data
5
- # only once they are attached to a Lightstreamer session using {Session#subscribe}.
2
+ # Describes a subscription that can be bound to a {Session} in order to consume its streaming data. A subscription is
3
+ # described by the options passed to {#initialize}. Incoming data can be consumed by registering an asynchronous data
4
+ # callback using {#add_data_callback}, or by polling {#retrieve_item_data}. Subscriptions start receiving data only
5
+ # once they are attached to a session using {Session#subscribe}.
6
6
  class Subscription
7
7
  # @return [Fixnum] The unique identification number of this subscription. This is used to identify the subscription
8
8
  # in incoming Lightstreamer data.
@@ -21,6 +21,13 @@ module Lightstreamer
21
21
  # If `nil` then the default data adapter will be used.
22
22
  attr_reader :adapter
23
23
 
24
+ # @return [String] The selector for table items. Optional.
25
+ attr_reader :selector
26
+
27
+ # @return [Float] The maximum number of updates this subscription should receive per second. If this is set to zero,
28
+ # which is the default, then there is no limit on the update frequency.
29
+ attr_reader :maximum_update_frequency
30
+
24
31
  # Initializes a new Lightstreamer subscription with the specified options. This can then be passed to
25
32
  # {Session#subscribe} to activate the subscription on a Lightstreamer session.
26
33
  #
@@ -30,13 +37,18 @@ module Lightstreamer
30
37
  # @option options [:distinct, :merge] :mode The operation mode of this subscription.
31
38
  # @option options [String] :adapter The name of the data adapter from the Lightstreamer session's adapter set that
32
39
  # should be used. If `nil` then the default data adapter will be used.
40
+ # @option options [String] :selector The selector for table items. Optional.
41
+ # @option options [Float] :maximum_update_frequency The maximum number of updates this subscription should receive
42
+ # per second. Defaults to zero which means there is no limit on the update frequency.
33
43
  def initialize(options)
34
44
  @id = self.class.next_id
35
45
 
36
- @items = Array(options.fetch(:items))
37
- @fields = Array(options.fetch(:fields))
46
+ @items = options.fetch(:items)
47
+ @fields = options.fetch(:fields)
38
48
  @mode = options.fetch(:mode).to_sym
39
49
  @adapter = options[:adapter]
50
+ @selector = options[:selector]
51
+ @maximum_update_frequency = options[:maximum_update_frequency] || 0.0
40
52
 
41
53
  @data_mutex = Mutex.new
42
54
  clear_data
@@ -3,20 +3,29 @@ module Lightstreamer
3
3
  module UTF16
4
4
  module_function
5
5
 
6
- # Decodes any UTF-16 escape sequences in the form '\uXXXX' into a new string. Invalid escape sequences are removed.
6
+ # Decodes any UTF-16 escape sequences in the form '\uXXXX' in the passed string. Invalid escape sequences are
7
+ # removed.
8
+ #
9
+ # @param [String] string The string to decode.
10
+ #
11
+ # @return [String]
7
12
  def decode_escape_sequences(string)
8
13
  string = decode_surrogate_pairs_escape_sequences string
9
14
 
10
- # Match all remaining escape sequences
15
+ # Match all escape sequences
11
16
  string.gsub(/\\u[A-F\d]{4}/i) do |escape_sequence|
12
17
  codepoint = escape_sequence[2..-1].hex
13
18
 
14
- # Codepoints greater than 0xD7FF are invalid
19
+ # Codepoints greater than 0xD7FF are invalid are ignored
15
20
  codepoint < 0xD800 ? [codepoint].pack('U') : ''
16
21
  end
17
22
  end
18
23
 
19
- # Converts any UTF-16 surrogate pairs escape sequences in the form '\uXXXX\uYYYY' into UTF-8.
24
+ # Decodes any UTF-16 surrogate pairs escape sequences in the form '\uXXXX\uYYYY' in the passed string.
25
+ #
26
+ # @param [String] string The string to decode.
27
+ #
28
+ # @return [String]
20
29
  def decode_surrogate_pairs_escape_sequences(string)
21
30
  string.gsub(/\\uD[89AB][A-F\d]{2}\\uD[C-F][A-F\d]{2}/i) do |escape_sequence|
22
31
  high_surrogate = escape_sequence[2...6].hex
@@ -1,4 +1,4 @@
1
1
  module Lightstreamer
2
2
  # The version of this gem.
3
- VERSION = '0.3'.freeze
3
+ VERSION = '0.4'.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.3'
4
+ version: '0.4'
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-07-24 00:00:00.000000000 Z
11
+ date: 2016-07-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -179,9 +179,8 @@ files:
179
179
  - lib/lightstreamer/cli/commands/stream_command.rb
180
180
  - lib/lightstreamer/cli/main.rb
181
181
  - lib/lightstreamer/control_connection.rb
182
+ - lib/lightstreamer/errors.rb
182
183
  - lib/lightstreamer/line_buffer.rb
183
- - lib/lightstreamer/protocol_error.rb
184
- - lib/lightstreamer/request_error.rb
185
184
  - lib/lightstreamer/session.rb
186
185
  - lib/lightstreamer/stream_connection.rb
187
186
  - lib/lightstreamer/stream_connection_header.rb
@@ -1,22 +0,0 @@
1
- module Lightstreamer
2
- # This error class is raised by {Session} when a request to the Lightstreamer API fails with a Lightstreamer-specific
3
- # error code and error message.
4
- class ProtocolError < StandardError
5
- # @return [String] A description of the Lightstreamer error that occurred.
6
- attr_reader :error
7
-
8
- # @return [Fixnum] The numeric code of the Lightstreamer error.
9
- attr_reader :code
10
-
11
- # Initializes this protocol error with the specific message and code.
12
- #
13
- # @param [String] error The error description.
14
- # @param [Integer] code The numeric code for the error.
15
- def initialize(error, code)
16
- @error = error.to_s
17
- @code = code.to_i
18
-
19
- super "Lightstreamer error: #{@error}, code: #{code}"
20
- end
21
- end
22
- end
@@ -1,21 +0,0 @@
1
- module Lightstreamer
2
- # This error class is raised by {Session} when a request to the Lightstreamer API fails.
3
- class RequestError < StandardError
4
- # @return [String] A description of the error that occurred when the request was attempted.
5
- attr_reader :error
6
-
7
- # @return [Fixnum] The HTTP code that was returned, or `nil` if unknown.
8
- attr_reader :http_code
9
-
10
- # Initializes this request error with a message and an HTTP code.
11
- #
12
- # @param [String] error The error description.
13
- # @param [Integer] http_code The HTTP code for the request failure, if known.
14
- def initialize(error, http_code = nil)
15
- @error = error.to_s
16
- @http_code = http_code ? http_code.to_i : nil
17
-
18
- super "Request error: #{error}#{http_code ? ", http code: #{http_code}" : ''}"
19
- end
20
- end
21
- end