lightstreamer 0.6 → 0.7
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 +4 -4
- data/CHANGELOG.md +21 -1
- data/README.md +5 -5
- data/lib/lightstreamer.rb +4 -2
- data/lib/lightstreamer/cli/commands/stream_command.rb +29 -15
- data/lib/lightstreamer/errors.rb +16 -0
- data/lib/lightstreamer/messages/end_of_snapshot_message.rb +27 -0
- data/lib/lightstreamer/messages/overflow_message.rb +2 -2
- data/lib/lightstreamer/messages/send_message_outcome_message.rb +53 -0
- data/lib/lightstreamer/messages/update_message.rb +2 -2
- data/lib/lightstreamer/post_request.rb +81 -0
- data/lib/lightstreamer/session.rb +145 -58
- data/lib/lightstreamer/{line_buffer.rb → stream_buffer.rb} +1 -1
- data/lib/lightstreamer/stream_connection.rb +18 -12
- data/lib/lightstreamer/subscription.rb +140 -65
- data/lib/lightstreamer/version.rb +1 -1
- metadata +8 -6
- data/lib/lightstreamer/control_connection.rb +0 -106
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 67492665643def7bc43ea5ab072416c91d7e494d
|
4
|
+
data.tar.gz: 3f47646a1ff2d3c939c65588304e57234f893490
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7510f00c805e9f83758e91b82c8682f22472d9c367a5a348faa4e46c8422e479c530bd96210c5eced1d7f56537ad39df14b6cf30dbf9d1a4787e1ea16695e6e5
|
7
|
+
data.tar.gz: 184073f14a2e2d34b9f208201526d6357a991d2a4bb113f40da21f80b112a0487f1177c12a24bd715a0e0c5b81d59997d9b6d64b603cff9c7ef89069e0455e02
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,26 @@
|
|
1
1
|
# Lightstreamer Changelog
|
2
2
|
|
3
|
-
### 0.
|
3
|
+
### 0.7 — July 31, 2016
|
4
|
+
|
5
|
+
- Refactored subscription handling to be more object-oriented, subscriptions are now created using
|
6
|
+
`Lightstreamer::Session#build_subscription` and there are new `#start`, `#stop` and `#unsilence` methods on
|
7
|
+
`Lightstreamer::Subscription` that control a subscription's current state
|
8
|
+
- Added `Lightstreamer::Session#bulk_subscription_start` for efficiently starting a number of subscriptions at once
|
9
|
+
- Added support for sending custom messages using `Lightstreamer::Session#send_message`, both synchronous and
|
10
|
+
asynchronous messages are supported, and the results of asynchronous messages can be checked using the new
|
11
|
+
`Lightstreamer::Session#on_message_result` callback
|
12
|
+
- Added support for setting initial subscription data
|
13
|
+
- Added support for changing a subscription's requested update frequency after it has been started
|
14
|
+
- Added support for setting and altering the session's requested maximum bandwidth
|
15
|
+
- Added support for requesting snapshots on subscriptions
|
16
|
+
- Added `Lightstreamer::Session#session_id` to query the session ID
|
17
|
+
- Added `Lightstreamer::Subscription#active` to query whether the subscription is active and streaming data
|
18
|
+
- Stream connection bind requests are now sent to the custom control address if one was specified in the stream
|
19
|
+
connection header
|
20
|
+
- The command-line client now prints the session ID following successful connection
|
21
|
+
- Added —requested-maximum-bandwidth option
|
22
|
+
|
23
|
+
### 0.6 — July 27, 2016
|
4
24
|
|
5
25
|
- Switched to the `excon` HTTP library
|
6
26
|
- Moved all subclasses of `Lightstreamer::LightstreamerError` into the `Lightstreamer::Errors` module
|
data/README.md
CHANGED
@@ -44,9 +44,9 @@ session = Lightstreamer::Session.new server_url: 'http://push.lightstreamer.com'
|
|
44
44
|
session.connect
|
45
45
|
|
46
46
|
# Create a new subscription that subscribes to thirty items and to four fields on each item
|
47
|
-
subscription =
|
48
|
-
|
49
|
-
|
47
|
+
subscription = session.build_subscription items: (1..30).map { |i| "item#{i}" },
|
48
|
+
fields: [:ask, :bid, :stock_name, :time],
|
49
|
+
mode: :merge, adapter: 'QUOTE_ADAPTER'
|
50
50
|
|
51
51
|
# Create a thread-safe queue
|
52
52
|
queue = Queue.new
|
@@ -57,8 +57,8 @@ subscription.on_data do |subscription, item_name, item_data, new_values|
|
|
57
57
|
queue.push item_data
|
58
58
|
end
|
59
59
|
|
60
|
-
#
|
61
|
-
|
60
|
+
# Start streaming data for the subscription
|
61
|
+
subscription.start
|
62
62
|
|
63
63
|
# Loop printing out new data as soon as it becomes available on the queue
|
64
64
|
loop do
|
data/lib/lightstreamer.rb
CHANGED
@@ -4,12 +4,14 @@ require 'uri'
|
|
4
4
|
|
5
5
|
require 'lightstreamer/cli/main'
|
6
6
|
require 'lightstreamer/cli/commands/stream_command'
|
7
|
-
require 'lightstreamer/control_connection'
|
8
7
|
require 'lightstreamer/errors'
|
9
|
-
require 'lightstreamer/
|
8
|
+
require 'lightstreamer/messages/end_of_snapshot_message'
|
10
9
|
require 'lightstreamer/messages/overflow_message'
|
10
|
+
require 'lightstreamer/messages/send_message_outcome_message'
|
11
11
|
require 'lightstreamer/messages/update_message'
|
12
|
+
require 'lightstreamer/post_request'
|
12
13
|
require 'lightstreamer/session'
|
14
|
+
require 'lightstreamer/stream_buffer'
|
13
15
|
require 'lightstreamer/stream_connection'
|
14
16
|
require 'lightstreamer/stream_connection_header'
|
15
17
|
require 'lightstreamer/subscription'
|
@@ -8,51 +8,57 @@ module Lightstreamer
|
|
8
8
|
option :username, desc: 'The username for the session'
|
9
9
|
option :password, desc: 'The password for the session'
|
10
10
|
option :adapter_set, desc: 'The name of the adapter set for the session'
|
11
|
+
option :requested_maximum_bandwidth, type: :numeric, desc: 'The requested maximum bandwidth, in kbps'
|
11
12
|
option :adapter, desc: 'The name of the data adapter to stream data from'
|
12
13
|
option :items, type: :array, required: true, desc: 'The names of the item(s) to stream'
|
13
14
|
option :fields, type: :array, required: true, desc: 'The field(s) to stream'
|
14
15
|
option :mode, enum: %w(distinct merge), default: :merge, desc: 'The operation mode'
|
15
16
|
option :selector, desc: 'The selector for table items'
|
17
|
+
option :snapshot, type: :boolean, desc: 'Whether to send snapshot data for the items'
|
16
18
|
option :maximum_update_frequency, desc: 'The maximum number of updates per second for each item'
|
17
19
|
|
18
20
|
def stream
|
19
|
-
session = create_session
|
20
|
-
session.connect
|
21
|
-
|
22
21
|
@queue = Queue.new
|
23
22
|
|
24
|
-
|
23
|
+
create_session
|
24
|
+
create_subscription
|
25
25
|
|
26
26
|
loop do
|
27
27
|
puts @queue.pop unless @queue.empty?
|
28
28
|
|
29
|
-
raise session.error if session.error
|
29
|
+
raise @session.error if @session.error
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
33
|
private
|
34
34
|
|
35
|
-
# Creates a new session from the specified options.
|
36
35
|
def create_session
|
37
|
-
Lightstreamer::Session.new
|
38
|
-
|
36
|
+
@session = Lightstreamer::Session.new session_options
|
37
|
+
@session.connect
|
38
|
+
@session.on_message_result(&method(:on_message_result))
|
39
|
+
|
40
|
+
puts "Session ID: #{@session.session_id}"
|
39
41
|
end
|
40
42
|
|
41
|
-
# Creates a new subscription from the specified options.
|
42
43
|
def create_subscription
|
43
|
-
subscription =
|
44
|
+
subscription = @session.build_subscription subscription_options
|
44
45
|
|
45
46
|
subscription.on_data(&method(:on_data))
|
46
47
|
subscription.on_overflow(&method(:on_overflow))
|
48
|
+
subscription.on_end_of_snapshot(&method(:on_end_of_snapshot))
|
49
|
+
|
50
|
+
subscription.start
|
51
|
+
end
|
47
52
|
|
48
|
-
|
53
|
+
def session_options
|
54
|
+
{ server_url: options[:server_url], username: options[:username], password: options[:password],
|
55
|
+
adapter_set: options[:adapter_set], requested_maximum_bandwidth: options[:requested_maximum_bandwidth] }
|
49
56
|
end
|
50
57
|
|
51
58
|
def subscription_options
|
52
|
-
{
|
53
|
-
|
54
|
-
|
55
|
-
}
|
59
|
+
{ items: options[:items], fields: options[:fields], mode: options[:mode], adapter: options[:adapter],
|
60
|
+
maximum_update_frequency: options[:maximum_update_frequency], selector: options[:selector],
|
61
|
+
snapshot: options[:snapshot] }
|
56
62
|
end
|
57
63
|
|
58
64
|
def on_data(_subscription, item_name, _item_data, new_values)
|
@@ -62,6 +68,14 @@ module Lightstreamer
|
|
62
68
|
def on_overflow(_subscription, item_name, overflow_size)
|
63
69
|
@queue.push "Overflow of size #{overflow_size} on item #{item_name}"
|
64
70
|
end
|
71
|
+
|
72
|
+
def on_message_result(sequence, numbers, error)
|
73
|
+
@queue.push "Message result for #{sequence}#{numbers} = #{error ? error.class : 'Done'}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def on_end_of_snapshot(_subscription, item_name)
|
77
|
+
@queue.push "End of snapshot for item #{item_name}"
|
78
|
+
end
|
65
79
|
end
|
66
80
|
end
|
67
81
|
end
|
data/lib/lightstreamer/errors.rb
CHANGED
@@ -95,6 +95,18 @@ module Lightstreamer
|
|
95
95
|
class InvalidProgressiveNumberError < LightstreamerError
|
96
96
|
end
|
97
97
|
|
98
|
+
# This error occurs when a send message request was refused as illegal by the metadata adapter.
|
99
|
+
class IllegalMessageError < LightstreamerError
|
100
|
+
end
|
101
|
+
|
102
|
+
# This error occurs when the elaboration of a sent message failed unexpectedly.
|
103
|
+
class MessageElaborationFailedError < LightstreamerError
|
104
|
+
end
|
105
|
+
|
106
|
+
# This error occurs when sent message(s) were not processed due to a timeout.
|
107
|
+
class MessagesSkippedByTimeoutError < LightstreamerError
|
108
|
+
end
|
109
|
+
|
98
110
|
# This error is raised when the client version requested is not supported by the server.
|
99
111
|
class ClientVersionNotSupportedError < LightstreamerError
|
100
112
|
end
|
@@ -209,6 +221,10 @@ module Lightstreamer
|
|
209
221
|
30 => Errors::SubscriptionsNotAllowedByLicenseError,
|
210
222
|
32 => Errors::InvalidProgressiveNumberError,
|
211
223
|
33 => Errors::InvalidProgressiveNumberError,
|
224
|
+
34 => Errors::IllegalMessageError,
|
225
|
+
35 => Errors::MessageElaborationFailedError,
|
226
|
+
38 => Errors::MessagesSkippedByTimeoutError,
|
227
|
+
39 => Errors::MessagesSkippedByTimeoutError,
|
212
228
|
60 => Errors::ClientVersionNotSupportedError
|
213
229
|
}.freeze
|
214
230
|
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Lightstreamer
|
2
|
+
# Helper class used by {Subscription} in order to parse incoming end-of-snapshot messages.
|
3
|
+
#
|
4
|
+
# @private
|
5
|
+
class EndOfSnapshotMessage
|
6
|
+
# The index of the item this end-of-snapshot message applies to.
|
7
|
+
#
|
8
|
+
# @return [Fixnum]
|
9
|
+
attr_accessor :item_index
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# Attempts to parses the specified line as an end-of-snapshot message for the given table and items and returns an
|
13
|
+
# instance of {EndOfSnapshotMessage} on success, or `nil` on failure.
|
14
|
+
def parse(line, table_id, items)
|
15
|
+
message = new
|
16
|
+
|
17
|
+
match = line.match Regexp.new("^#{table_id},(\\d+),EOS$")
|
18
|
+
return unless match
|
19
|
+
|
20
|
+
message.item_index = match.captures[0].to_i - 1
|
21
|
+
return unless message.item_index < items.size
|
22
|
+
|
23
|
+
message
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -17,11 +17,11 @@ module Lightstreamer
|
|
17
17
|
# Attempts to parses the specified line as an overflow message for the given table and items and returns an
|
18
18
|
# instance of {OverflowMessage} on success, or `nil` on failure.
|
19
19
|
def parse(line, table_id, items)
|
20
|
-
message = new
|
21
|
-
|
22
20
|
match = line.match table_regexp(table_id)
|
23
21
|
return unless match
|
24
22
|
|
23
|
+
message = new
|
24
|
+
|
25
25
|
message.item_index = match.captures[0].to_i - 1
|
26
26
|
return unless message.item_index < items.size
|
27
27
|
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Lightstreamer
|
2
|
+
# Helper class used by {Session} in order to parse incoming overflow send message outcome messages.
|
3
|
+
#
|
4
|
+
# @private
|
5
|
+
class SendMessageOutcomeMessage
|
6
|
+
# The name of the sequence this message outcome is for.
|
7
|
+
#
|
8
|
+
# @return [String]
|
9
|
+
attr_accessor :sequence
|
10
|
+
|
11
|
+
# The message number(s) this message outcome is for. There will always be exactly one entry in this array except in
|
12
|
+
# the case where {#error} is a {MessagesSkippedByTimeoutError} in which case there may be more than one entry if
|
13
|
+
# multiple messages were skipped.
|
14
|
+
#
|
15
|
+
# @return [Array<Fixnum>]
|
16
|
+
attr_accessor :numbers
|
17
|
+
|
18
|
+
# If an error occurred processing the message then it will be set here.
|
19
|
+
#
|
20
|
+
# @return [LightstreamerError, nil]
|
21
|
+
attr_accessor :error
|
22
|
+
|
23
|
+
class << self
|
24
|
+
# Attempts to parses the specified line as a message outcome message and returns an instance of
|
25
|
+
# {SendMessageOutcomeMessage} on success, or `nil` on failure.
|
26
|
+
def parse(line)
|
27
|
+
match = line.match Regexp.new '^MSG,([A-Za-z0-9_]+),(\d*),(?:DONE|ERR,(\d*),(.*))$'
|
28
|
+
return unless match
|
29
|
+
|
30
|
+
message = new
|
31
|
+
|
32
|
+
message.sequence = match.captures[0]
|
33
|
+
message.numbers = [match.captures[1].to_i]
|
34
|
+
handle_error_outcome message, match.captures if match.captures.compact.size == 4
|
35
|
+
|
36
|
+
message
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def handle_error_outcome(message, captures)
|
42
|
+
message.error = LightstreamerError.build captures[3], captures[2]
|
43
|
+
|
44
|
+
return unless captures[2].to_i == 39
|
45
|
+
|
46
|
+
last_number = message.numbers[0]
|
47
|
+
first_number = last_number - captures[3].to_i + 1
|
48
|
+
|
49
|
+
message.numbers = Array(first_number..last_number)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -17,11 +17,11 @@ module Lightstreamer
|
|
17
17
|
# Attempts to parses the specified line as an update message for the given table, items, and fields, and returns
|
18
18
|
# an instance of {UpdateMessage} on success, or `nil` on failure.
|
19
19
|
def parse(line, table_id, items, fields)
|
20
|
-
message = new
|
21
|
-
|
22
20
|
match = line.match table_regexp(table_id, fields)
|
23
21
|
return unless match
|
24
22
|
|
23
|
+
message = new
|
24
|
+
|
25
25
|
message.item_index = match.captures[0].to_i - 1
|
26
26
|
return unless message.item_index < items.size
|
27
27
|
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Lightstreamer
|
2
|
+
# This module contains helper methods for sending single and bulk POST requests to a Lightstreamer server and
|
3
|
+
# handling the possible error responses.
|
4
|
+
#
|
5
|
+
# @private
|
6
|
+
module PostRequest
|
7
|
+
module_function
|
8
|
+
|
9
|
+
# Sends a POST request to the specified Lightstreamer URL with the given query params. If an error occurs then a
|
10
|
+
# {LightstreamerError} subclass will be raised.
|
11
|
+
#
|
12
|
+
# @param [String] url The URL to send the POST request to.
|
13
|
+
# @param [Hash] query The POST request's query params.
|
14
|
+
def execute(url, query)
|
15
|
+
errors = bulk_execute url, [request_body(query)]
|
16
|
+
raise errors.first if errors.first
|
17
|
+
end
|
18
|
+
|
19
|
+
# Sends a POST request to the specified Lightstreamer URL that concatenates multiple individual POST request bodies
|
20
|
+
# into one to avoid sending lots of individual requests. The return value is an array with one entry per body and
|
21
|
+
# indicates the error state returned by the server for that body's request, or `nil` if no error occurred.
|
22
|
+
#
|
23
|
+
# @param [String] url The URL to send the POST request to.
|
24
|
+
# @param [Array<String>] bodies The individual POST request bodies that are to be sent together in one request.
|
25
|
+
# These should be created with {#request_body}.
|
26
|
+
#
|
27
|
+
# @return [Array<LightstreamerError, nil>] The execution result of each of the passed bodies. If an entry is `nil`
|
28
|
+
# then no error occurred when executing that body.
|
29
|
+
def bulk_execute(url, bodies)
|
30
|
+
response = Excon.post url, body: bodies.join("\r\n"), expects: 200, connect_timeout: 15
|
31
|
+
|
32
|
+
response_lines = response.body.split("\n").map(&:strip)
|
33
|
+
|
34
|
+
errors = []
|
35
|
+
errors << parse_error(response_lines) until response_lines.empty?
|
36
|
+
|
37
|
+
raise LightstreamerError if errors.size != bodies.size
|
38
|
+
|
39
|
+
errors
|
40
|
+
rescue Excon::Error => error
|
41
|
+
raise Errors::ConnectionError, error.message
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns the request body to send for a POST request with the given options.
|
45
|
+
#
|
46
|
+
# @param [Hash] query The POST request's query params.
|
47
|
+
#
|
48
|
+
# @return [String] The request body for the given query params.
|
49
|
+
def request_body(query)
|
50
|
+
params = {}
|
51
|
+
|
52
|
+
query.each do |key, value|
|
53
|
+
next if value.nil?
|
54
|
+
value = value.map(&:to_s).join(' ') if value.is_a? Array
|
55
|
+
params[key] = value
|
56
|
+
end
|
57
|
+
|
58
|
+
URI.encode_www_form params
|
59
|
+
end
|
60
|
+
|
61
|
+
# Parses the next error from the given lines that were returned by a POST request. The consumed lines are removed
|
62
|
+
# from the passed array.
|
63
|
+
#
|
64
|
+
# @param [Array<String>] response_lines
|
65
|
+
#
|
66
|
+
# @return [LightstreamerError, nil]
|
67
|
+
def parse_error(response_lines)
|
68
|
+
first_line = response_lines.shift
|
69
|
+
|
70
|
+
return nil if first_line == 'OK'
|
71
|
+
return Errors::SyncError.new if first_line == 'SYNC ERROR'
|
72
|
+
|
73
|
+
if first_line == 'ERROR'
|
74
|
+
error_code = response_lines.shift
|
75
|
+
LightstreamerError.build response_lines.shift, error_code
|
76
|
+
else
|
77
|
+
LightstreamerError.new first_line
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Lightstreamer
|
2
|
-
# This class is responsible for managing a Lightstreamer session, and along with the {Subscription} class
|
3
|
-
# primary
|
2
|
+
# This class is responsible for managing a Lightstreamer session, and along with the {Subscription} class forms the
|
3
|
+
# primary API for working with Lightstreamer.
|
4
4
|
class Session
|
5
5
|
# The URL of the Lightstreamer server to connect to. Set by {#initialize}.
|
6
6
|
#
|
@@ -24,11 +24,16 @@ module Lightstreamer
|
|
24
24
|
|
25
25
|
# If an error occurs on the stream connection that causes the session to terminate then details of the error will be
|
26
26
|
# stored in this attribute. If the session is terminated as a result of calling {#disconnect} then the error will be
|
27
|
-
# {SessionEndError}.
|
27
|
+
# {Errors::SessionEndError}.
|
28
28
|
#
|
29
29
|
# @return [LightstreamerError, nil]
|
30
30
|
attr_reader :error
|
31
31
|
|
32
|
+
# The server-side bandwidth constraint on data usage, expressed in kbps. If this is zero then no limit is applied.
|
33
|
+
#
|
34
|
+
# @return [Float]
|
35
|
+
attr_reader :requested_maximum_bandwidth
|
36
|
+
|
32
37
|
# Initializes this new Lightstreamer session with the passed options.
|
33
38
|
#
|
34
39
|
# @param [Hash] options The options to create the session with.
|
@@ -36,6 +41,8 @@ module Lightstreamer
|
|
36
41
|
# @option options [String] :username The username to connect to the server with.
|
37
42
|
# @option options [String] :password The password to connect to the server with.
|
38
43
|
# @option options [String] :adapter_set The name of the adapter set to request from the server.
|
44
|
+
# @option options [Float] :requested_maximum_bandwidth. The server-side bandwidth constraint on data usage,
|
45
|
+
# expressed in kbps. Defaults to zero which means no limit is applied.
|
39
46
|
def initialize(options = {})
|
40
47
|
@subscriptions = []
|
41
48
|
@subscriptions_mutex = Mutex.new
|
@@ -44,41 +51,55 @@ module Lightstreamer
|
|
44
51
|
@username = options[:username]
|
45
52
|
@password = options[:password]
|
46
53
|
@adapter_set = options[:adapter_set]
|
54
|
+
@requested_maximum_bandwidth = options[:requested_maximum_bandwidth].to_f
|
55
|
+
|
56
|
+
@on_message_result_callbacks = []
|
47
57
|
end
|
48
58
|
|
49
|
-
#
|
59
|
+
# Connects a new Lightstreamer session using the details passed to {#initialize}. If an error occurs then
|
50
60
|
# a {LightstreamerError} subclass will be raised.
|
51
61
|
def connect
|
52
62
|
return if @stream_connection
|
53
63
|
|
54
64
|
@error = nil
|
55
65
|
|
56
|
-
|
57
|
-
|
66
|
+
@stream_connection = StreamConnection.new self
|
67
|
+
@stream_connection.connect
|
68
|
+
|
58
69
|
create_processing_thread
|
59
70
|
rescue
|
60
71
|
@stream_connection = nil
|
61
72
|
raise
|
62
73
|
end
|
63
74
|
|
64
|
-
# Returns whether this session is currently connected and has an active stream connection.
|
75
|
+
# Returns whether this Lightstreamer session is currently connected and has an active stream connection.
|
65
76
|
#
|
66
77
|
# @return [Boolean]
|
67
78
|
def connected?
|
68
79
|
!@stream_connection.nil?
|
69
80
|
end
|
70
81
|
|
71
|
-
#
|
82
|
+
# Returns the ID of the currently active Lightstreamer session, or `nil` if there is no active session.
|
83
|
+
#
|
84
|
+
# @return [String, nil]
|
85
|
+
def session_id
|
86
|
+
@stream_connection && @stream_connection.session_id
|
87
|
+
end
|
88
|
+
|
89
|
+
# Disconnects this Lightstreamer session and terminates the session on the server. All worker threads are exited.
|
72
90
|
def disconnect
|
73
|
-
|
91
|
+
control_request :destroy if @stream_connection
|
74
92
|
|
75
93
|
@processing_thread.join 5 if @processing_thread
|
76
94
|
ensure
|
77
95
|
@stream_connection.disconnect if @stream_connection
|
78
96
|
@processing_thread.exit if @processing_thread
|
79
97
|
|
80
|
-
@
|
81
|
-
|
98
|
+
@subscriptions.each do |subscription|
|
99
|
+
subscription.instance_variable_set :@active, false
|
100
|
+
end
|
101
|
+
|
102
|
+
@processing_thread = @stream_connection = nil
|
82
103
|
end
|
83
104
|
|
84
105
|
# Requests that the Lightstreamer server terminate the currently active stream connection and require that a new
|
@@ -88,66 +109,123 @@ module Lightstreamer
|
|
88
109
|
def force_rebind
|
89
110
|
return unless @stream_connection
|
90
111
|
|
91
|
-
|
112
|
+
control_request :force_rebind
|
92
113
|
end
|
93
114
|
|
94
|
-
#
|
95
|
-
# {
|
115
|
+
# Builds a new subscription for this session with the specified options. Note that ths does not activate the
|
116
|
+
# subscription, {Subscription#start} must be called to actually start streaming the subscription's data. See the
|
117
|
+
# {Subscription} class for more details.
|
118
|
+
#
|
119
|
+
# @param [Hash] options The options to create the subscription with.
|
120
|
+
# @option options [Array] :items The names of the items to subscribe to. Required.
|
121
|
+
# @option options [Array] :fields The names of the fields to subscribe to on the items. Required.
|
122
|
+
# @option options [:distinct, :merge] :mode The operation mode of the subscription. Required.
|
123
|
+
# @option options [String] :adapter The name of the data adapter from this session's adapter set that should be
|
124
|
+
# used. If `nil` then the default data adapter will be used.
|
125
|
+
# @option options [String] :selector The selector for table items. Optional.
|
126
|
+
# @option options [Float, :unfiltered] :maximum_update_frequency The maximum number of updates the subscription
|
127
|
+
# should receive per second. Defaults to zero which means there is no limit on the update frequency.
|
128
|
+
# If set to `:unfiltered` then unfiltered streaming will be used for the subscription and it is
|
129
|
+
# possible for overflows to occur (see {Subscription#on_overflow}).
|
96
130
|
#
|
97
|
-
# @
|
98
|
-
def
|
99
|
-
subscription.
|
131
|
+
# @return [Subscription] The new subscription.
|
132
|
+
def build_subscription(options)
|
133
|
+
subscription = Subscription.new self, options
|
100
134
|
|
101
135
|
@subscriptions_mutex.synchronize { @subscriptions << subscription }
|
102
136
|
|
103
|
-
|
104
|
-
|
105
|
-
|
137
|
+
subscription
|
138
|
+
end
|
139
|
+
|
140
|
+
# Stops the specified subscription and removes it from this session. If an error occurs then a {LightstreamerError}
|
141
|
+
# subclass will be raised. To just stop a subscription with the option of restarting it at a later date call
|
142
|
+
# {Subscription#stop} on the subscription itself.
|
143
|
+
def remove_subscription(subscription)
|
144
|
+
subscription.stop
|
106
145
|
|
107
|
-
@control_connection.subscription_execute :add, subscription.id, options
|
108
|
-
rescue
|
109
146
|
@subscriptions_mutex.synchronize { @subscriptions.delete subscription }
|
110
|
-
raise
|
111
147
|
end
|
112
148
|
|
113
|
-
#
|
114
|
-
#
|
115
|
-
#
|
149
|
+
# This method performs a bulk {Subscription#start} on all the passed subscriptions. Calling {Subscription#start} on
|
150
|
+
# each of them individually would also work but requires a separate POST request to be sent for every subscription.
|
151
|
+
# This request starts all of the passed subscriptions in a single POST request which is significantly faster for
|
152
|
+
# a large number of subscriptions. The return value is an array with one entry per subscription and indicates the
|
153
|
+
# error state returned by the server for that subscription's start request, or `nil` if no error occurred.
|
116
154
|
#
|
117
|
-
# @
|
118
|
-
def subscribed?(subscription)
|
119
|
-
@subscriptions_mutex.synchronize { @subscriptions.include? subscription }
|
120
|
-
end
|
121
|
-
|
122
|
-
# Unsubscribes this Lightstreamer session from the specified subscription.
|
155
|
+
# @param [Array<Subscription>] subscriptions The subscriptions to start.
|
123
156
|
#
|
124
|
-
# @
|
125
|
-
def
|
126
|
-
|
157
|
+
# @return [Array<LightstreamerError, nil>]
|
158
|
+
def bulk_subscription_start(*subscriptions)
|
159
|
+
request_bodies = subscriptions.map do |subscription|
|
160
|
+
PostRequest.request_body session_id, *subscription.start_control_request_args
|
161
|
+
end
|
127
162
|
|
128
|
-
@
|
163
|
+
errors = PostRequest.bulk_execute @stream_connection.control_address, request_bodies
|
129
164
|
|
130
|
-
@
|
165
|
+
# Set @active to true on all subscriptions that did not have an error
|
166
|
+
errors.each_with_index do |error, index|
|
167
|
+
subscriptions[index].instance_variable_set :@active, true if error.nil?
|
168
|
+
end
|
131
169
|
end
|
132
170
|
|
133
|
-
|
171
|
+
# Sets the server-side bandwidth constraint on data usage for this session, expressed in kbps. A value of zero
|
172
|
+
# means no limit will be applied. If an error occurs then a {LightstreamerError} subclass will be raised.
|
173
|
+
#
|
174
|
+
# @param [Float] bandwidth The new requested maximum bandwidth, expressed in kbps.
|
175
|
+
def requested_maximum_bandwidth=(bandwidth)
|
176
|
+
control_request :constrain, LS_requested_max_bandwidth: bandwidth if connected?
|
177
|
+
@requested_maximum_bandwidth = bandwidth.to_f
|
178
|
+
end
|
134
179
|
|
135
|
-
|
136
|
-
|
137
|
-
|
180
|
+
# Sends a custom message to the Lightstreamer server. Message sending can be done synchronously or asynchronously.
|
181
|
+
# By default the message will be sent synchronously, i.e. the message will be processed by the server and if an
|
182
|
+
# error occurs a {LightstreamerError} subclass will be raised immediately. However, if the `:async` option is true
|
183
|
+
# then the message will be sent asynchronously, and the result of the message send will be reported to all callbacks
|
184
|
+
# that have been registered via {#on_message_result}.
|
185
|
+
#
|
186
|
+
# @param [String] message The message to send to the Lightstreamer server.
|
187
|
+
# @param [Hash] options The options that control messages sent asynchronously.
|
188
|
+
# @option options [Boolean] :async Whether to send the message asynchronously. Defaults to `false`.
|
189
|
+
# @option options [String] :sequence The alphanumeric identifier that identifies a subset of messages that are to
|
190
|
+
# be processed in sequence based on the `:number` given to them. If the special `UNORDERED_MESSAGES`
|
191
|
+
# sequence is used then the associated messages are processed immediately, possibly concurrently,
|
192
|
+
# with no ordering constraint.
|
193
|
+
# @option options [Fixnum] :number The progressive number of this message within its sequence. Should start at 1.
|
194
|
+
# @option options [Float] :max_wait The maximum time the server can wait before processing this message if one or
|
195
|
+
# more of the preceding messages in the same sequence have not been received. If not specified then
|
196
|
+
# a timeout is assigned by the server.
|
197
|
+
def send_message(message, options = {})
|
198
|
+
url = URI.join(@stream_connection.control_address, '/lightstreamer/send_message.txt').to_s
|
199
|
+
|
200
|
+
query = { LS_session: session_id, LS_message: message }
|
201
|
+
query[:LS_sequence] = options.fetch(:sequence) if options[:async]
|
202
|
+
query[:LS_msg_prog] = options.fetch(:number) if options[:async]
|
203
|
+
query[:LS_max_wait] = options[:max_wait] if options[:max_wait]
|
204
|
+
|
205
|
+
PostRequest.execute url, query
|
138
206
|
end
|
139
207
|
|
140
|
-
|
141
|
-
|
208
|
+
# Adds the passed block to the list of callbacks that will be run when the outcome of an asynchronous message send
|
209
|
+
# arrives. The block will be called on a worker thread and so the code that is run by the block must be thread-safe.
|
210
|
+
# The arguments passed to the block are `|sequence, numbers, error|`.
|
211
|
+
#
|
212
|
+
# @param [Proc] callback The callback that is to be run.
|
213
|
+
def on_message_result(&callback)
|
214
|
+
@on_message_result_callbacks << callback
|
215
|
+
end
|
142
216
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
217
|
+
# Sends a request to the control connection. If an error occurs then a {LightstreamerError} subclass will be raised.
|
218
|
+
#
|
219
|
+
# @param [Symbol] operation The control operation to perform.
|
220
|
+
# @param [Hash] options The options to send with the control request.
|
221
|
+
def control_request(operation, options = {})
|
222
|
+
url = URI.join(@stream_connection.control_address, '/lightstreamer/control.txt').to_s
|
147
223
|
|
148
|
-
|
224
|
+
PostRequest.execute url, options.merge(LS_session: session_id, LS_op: operation)
|
149
225
|
end
|
150
226
|
|
227
|
+
private
|
228
|
+
|
151
229
|
# Starts the processing thread that reads and processes incoming data from the stream connection.
|
152
230
|
def create_processing_thread
|
153
231
|
@processing_thread = Thread.new do
|
@@ -155,28 +233,37 @@ module Lightstreamer
|
|
155
233
|
|
156
234
|
loop do
|
157
235
|
line = @stream_connection.read_line
|
158
|
-
|
159
236
|
break if line.nil?
|
160
237
|
|
161
|
-
|
238
|
+
process_stream_line line
|
162
239
|
end
|
163
240
|
|
164
241
|
# The stream connection has terminated so the session is assumed to be over
|
165
242
|
@error = @stream_connection.error
|
166
|
-
@processing_thread = @
|
243
|
+
@processing_thread = @stream_connection = nil
|
167
244
|
end
|
168
245
|
end
|
169
246
|
|
170
|
-
# Processes a single line of incoming stream data by passing it to all the
|
247
|
+
# Processes a single line of incoming stream data by passing it to all the subscriptions until one
|
171
248
|
# successfully processes it. This method is always run on the processing thread.
|
172
|
-
def
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
249
|
+
def process_stream_line(line)
|
250
|
+
return if @subscriptions.any? { |subscription| subscription.process_stream_data line }
|
251
|
+
return if process_send_message_outcome line
|
252
|
+
|
253
|
+
warn "Lightstreamer: unprocessed stream data '#{line}'"
|
254
|
+
end
|
255
|
+
|
256
|
+
# Attempts to process the passed line as a send message outcome message, and if is such a message then the
|
257
|
+
# registered callbacks are run.
|
258
|
+
def process_send_message_outcome(line)
|
259
|
+
outcome = SendMessageOutcomeMessage.parse line
|
260
|
+
return unless outcome
|
261
|
+
|
262
|
+
@on_message_result_callbacks.each do |callback|
|
263
|
+
callback.call outcome.sequence, outcome.numbers, outcome.error
|
177
264
|
end
|
178
265
|
|
179
|
-
|
266
|
+
true
|
180
267
|
end
|
181
268
|
end
|
182
269
|
end
|
@@ -9,7 +9,7 @@ module Lightstreamer
|
|
9
9
|
# @return [String, nil]
|
10
10
|
attr_reader :session_id
|
11
11
|
|
12
|
-
# The control address
|
12
|
+
# The control address to use for this stream connection.
|
13
13
|
#
|
14
14
|
# @return [String, nil]
|
15
15
|
attr_reader :control_address
|
@@ -27,9 +27,6 @@ module Lightstreamer
|
|
27
27
|
@session = session
|
28
28
|
@queue = Queue.new
|
29
29
|
|
30
|
-
@stream_create_url = URI.join(session.server_url, '/lightstreamer/create_session.txt').to_s
|
31
|
-
@stream_bind_url = URI.join(session.server_url, '/lightstreamer/bind_session.txt').to_s
|
32
|
-
|
33
30
|
@connect_result_mutex = Mutex.new
|
34
31
|
@connect_result_condition_variable = ConditionVariable.new
|
35
32
|
end
|
@@ -99,26 +96,29 @@ module Lightstreamer
|
|
99
96
|
|
100
97
|
def create_new_stream
|
101
98
|
params = { LS_op2: 'create', LS_cid: 'mgQkwtwdysogQz2BJ4Ji kOj2Bg', LS_user: @session.username,
|
102
|
-
LS_password: @session.password }
|
99
|
+
LS_password: @session.password, LS_requested_max_bandwidth: @session.requested_maximum_bandwidth }
|
103
100
|
|
104
101
|
params[:LS_adapter_set] = @session.adapter_set if @session.adapter_set
|
105
102
|
|
106
|
-
|
103
|
+
url = URI.join(@session.server_url, '/lightstreamer/create_session.txt').to_s
|
104
|
+
execute_stream_post_request url, connect_timeout: 15, query: params
|
107
105
|
|
108
106
|
signal_connect_result_ready
|
109
107
|
end
|
110
108
|
|
111
109
|
def bind_to_existing_stream
|
112
|
-
|
110
|
+
params = { LS_session: @session_id, LS_requested_max_bandwidth: @session.requested_maximum_bandwidth }
|
111
|
+
|
112
|
+
url = URI.join(control_address, '/lightstreamer/bind_session.txt').to_s
|
113
|
+
execute_stream_post_request url, connect_timeout: 15, query: params
|
113
114
|
end
|
114
115
|
|
115
116
|
def execute_stream_post_request(url, options)
|
116
117
|
@header = StreamConnectionHeader.new
|
117
118
|
|
118
|
-
buffer =
|
119
|
-
options[:response_block] =
|
120
|
-
|
121
|
-
end
|
119
|
+
buffer = StreamBuffer.new
|
120
|
+
options[:response_block] = -> (data, _, _) { buffer.process data, &method(:process_stream_line) }
|
121
|
+
options[:expects] = 200
|
122
122
|
|
123
123
|
Excon.post url, options
|
124
124
|
rescue Excon::Error => error
|
@@ -141,7 +141,13 @@ module Lightstreamer
|
|
141
141
|
header_incomplete = @header.process_line line
|
142
142
|
|
143
143
|
@session_id = @header['SessionId']
|
144
|
-
|
144
|
+
|
145
|
+
# Set the control address and ensure it has a schema
|
146
|
+
@control_address = @header['ControlAddress'] || @session.server_url
|
147
|
+
unless @control_address.start_with? 'http'
|
148
|
+
@control_address = "#{URI(@session.server_url).scheme}://#{@control_address}"
|
149
|
+
end
|
150
|
+
|
145
151
|
@error = @header.error
|
146
152
|
|
147
153
|
return if header_incomplete
|
@@ -1,13 +1,13 @@
|
|
1
1
|
module Lightstreamer
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
2
|
+
# This class manages a subscription that can be used to stream data from a {Session}. Subscriptions should always be
|
3
|
+
# created using {Session#build_subscription}. Subscriptions start receiving data after {#start} is called, and
|
4
|
+
# streaming subscription data can be consumed by registering an asynchronous data callback using {#on_data}, or by
|
5
|
+
# polling using {#item_data}.
|
6
6
|
class Subscription
|
7
|
-
# The
|
7
|
+
# The session that this subscription is associated with.
|
8
8
|
#
|
9
|
-
# @return [
|
10
|
-
attr_reader :
|
9
|
+
# @return [Session]
|
10
|
+
attr_reader :session
|
11
11
|
|
12
12
|
# The names of the items to subscribe to.
|
13
13
|
#
|
@@ -42,29 +42,26 @@ module Lightstreamer
|
|
42
42
|
# @return [Float, :unfiltered]
|
43
43
|
attr_reader :maximum_update_frequency
|
44
44
|
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
|
49
|
-
|
50
|
-
#
|
51
|
-
#
|
52
|
-
# @
|
53
|
-
#
|
54
|
-
#
|
55
|
-
# @
|
56
|
-
|
57
|
-
|
58
|
-
# possible for overflows to occur (see {#on_overflow}).
|
59
|
-
def initialize(options)
|
60
|
-
@id = self.class.next_id
|
45
|
+
# Whether this subscription is currently started and actively streaming data. See {#start} and {#stop} for details.
|
46
|
+
#
|
47
|
+
# @return [Boolean]
|
48
|
+
attr_reader :active
|
49
|
+
|
50
|
+
# Initializes a new Lightstreamer subscription with the specified options.
|
51
|
+
#
|
52
|
+
# @param [Session] session The session this subscription is associated with.
|
53
|
+
# @param [Hash] options The options to create the subscription with. See {Session#build_subscription}
|
54
|
+
#
|
55
|
+
# @private
|
56
|
+
def initialize(session, options)
|
57
|
+
@session = session
|
61
58
|
|
62
59
|
@items = options.fetch(:items)
|
63
60
|
@fields = options.fetch(:fields)
|
64
61
|
@mode = options.fetch(:mode).to_sym
|
65
62
|
@adapter = options[:adapter]
|
66
63
|
@selector = options[:selector]
|
67
|
-
@maximum_update_frequency = options[:maximum_update_frequency]
|
64
|
+
@maximum_update_frequency = sanitize_frequency options[:maximum_update_frequency]
|
68
65
|
|
69
66
|
@data_mutex = Mutex.new
|
70
67
|
|
@@ -72,6 +69,76 @@ module Lightstreamer
|
|
72
69
|
clear_callbacks
|
73
70
|
end
|
74
71
|
|
72
|
+
# Returns this subscription's unique identification number.
|
73
|
+
#
|
74
|
+
# @return [Fixnum]
|
75
|
+
#
|
76
|
+
# @private
|
77
|
+
def id
|
78
|
+
@id ||= ID_GENERATOR.next
|
79
|
+
end
|
80
|
+
|
81
|
+
# Starts streaming data for this Lightstreamer subscription. If an error occurs then a {LightstreamerError} subclass
|
82
|
+
# will be raised.
|
83
|
+
#
|
84
|
+
# @param [Hash] options The options to start the subscription with.
|
85
|
+
# @option options [Boolean] :silent Whether the subscription should be started in silent mode. In silent mode the
|
86
|
+
# subscription is initiated on the server and begins buffering incoming data, however this data will
|
87
|
+
# not be sent to the client for processing until {#unsilence} is called.
|
88
|
+
# @option options [Boolean, Fixnum] :snapshot Controls whether the server should send a snapshot of this
|
89
|
+
# subscription's items. If `false` then the server does not send snapshot information (this is the
|
90
|
+
# default). If `true` then the server will send snapshot information if it's available. If this
|
91
|
+
# subscription's {#mode} is `:distinct` then `:snapshot` can also be an integer specifying the
|
92
|
+
# number of events the server should send as part of the snapshot. If this latter option is used
|
93
|
+
# then any callbacks registered with {#on_end_of_snapshot} will be called once the snapshot for each
|
94
|
+
# item is complete.
|
95
|
+
def start(options = {})
|
96
|
+
session.control_request(*start_control_request_args(options)) unless @active
|
97
|
+
@active = true
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns the arguments to pass to to {Session#control_request} in order ot start this subscription with the given
|
101
|
+
# options.
|
102
|
+
#
|
103
|
+
# @param [Hash] options The options to start the subscription with.
|
104
|
+
#
|
105
|
+
# @private
|
106
|
+
def start_control_request_args(options = {})
|
107
|
+
operation = options[:silent] ? :add_silent : :add
|
108
|
+
|
109
|
+
options = { LS_table: id, LS_mode: mode.to_s.upcase, LS_id: items, LS_schema: fields, LS_data_adapter: adapter,
|
110
|
+
LS_requested_max_frequency: maximum_update_frequency, LS_selector: selector,
|
111
|
+
LS_snapshot: options.fetch(:snapshot, false) }
|
112
|
+
|
113
|
+
[operation, options]
|
114
|
+
end
|
115
|
+
|
116
|
+
# Unsilences this subscription if it was initially started in silent mode (by passing `silent: true` to {#start}).
|
117
|
+
# If this subscription was not started in silent mode then this method has no effect. If an error occurs then a
|
118
|
+
# {LightstreamerError} subclass will be raised.
|
119
|
+
def unsilence
|
120
|
+
session.control_request :start, LS_table: id
|
121
|
+
end
|
122
|
+
|
123
|
+
# Stops streaming data for this Lightstreamer subscription. If an error occurs then a {LightstreamerError} subclass
|
124
|
+
# will be raised.
|
125
|
+
def stop
|
126
|
+
session.control_request :delete, LS_table: id if @active
|
127
|
+
@active = false
|
128
|
+
end
|
129
|
+
|
130
|
+
# Sets this subscription's maximum update frequency. This can be done while a subscription is streaming data in
|
131
|
+
# order to change its update frequency limit, but an actively streaming subscription cannot switch between filtered
|
132
|
+
# and unfiltered dispatching, and {TableModificationNotAllowedError} will be raised if this is attempted.
|
133
|
+
#
|
134
|
+
# @param [Float, :unfiltered] new_frequency The new maximum update frequency. See {#maximum_update_frequency} for
|
135
|
+
# details.
|
136
|
+
def maximum_update_frequency=(new_frequency)
|
137
|
+
new_frequency = sanitize_frequency new_frequency
|
138
|
+
session.control_request :reconf, LS_table: id, LS_requested_max_frequency: new_frequency if @active
|
139
|
+
@maximum_update_frequency = new_frequency
|
140
|
+
end
|
141
|
+
|
75
142
|
# Clears all current data stored for this subscription. New data will continue to be processed as it becomes
|
76
143
|
# available.
|
77
144
|
def clear_data
|
@@ -85,9 +152,7 @@ module Lightstreamer
|
|
85
152
|
#
|
86
153
|
# @param [Proc] callback The callback that is to be run when new data arrives.
|
87
154
|
def on_data(&callback)
|
88
|
-
@data_mutex.synchronize
|
89
|
-
@callbacks[:on_data] << callback
|
90
|
-
end
|
155
|
+
@data_mutex.synchronize { @callbacks[:on_data] << callback }
|
91
156
|
end
|
92
157
|
|
93
158
|
# Adds the passed block to the list of callbacks that will be run when the server reports an overflow for this
|
@@ -96,16 +161,21 @@ module Lightstreamer
|
|
96
161
|
#
|
97
162
|
# @param [Proc] callback The callback that is to be run when an overflow is reported for this subscription.
|
98
163
|
def on_overflow(&callback)
|
99
|
-
@data_mutex.synchronize
|
100
|
-
|
101
|
-
|
164
|
+
@data_mutex.synchronize { @callbacks[:on_overflow] << callback }
|
165
|
+
end
|
166
|
+
|
167
|
+
# Adds the passed block to the list of callbacks that will be run when the server reports an end-of-snapshot
|
168
|
+
# notification for this subscription. The block will be called on a worker thread and so the code that is run by the
|
169
|
+
# block must be thread-safe. The arguments passed to the block are `|subscription, item_name|`.
|
170
|
+
#
|
171
|
+
# @param [Proc] callback The callback that is to be run when an overflow is reported for this subscription.
|
172
|
+
def on_end_of_snapshot(&callback)
|
173
|
+
@data_mutex.synchronize { @callbacks[:on_end_of_snapshot] << callback }
|
102
174
|
end
|
103
175
|
|
104
176
|
# Removes all {#on_data} and {#on_overflow} callbacks present on this subscription.
|
105
177
|
def clear_callbacks
|
106
|
-
@data_mutex.synchronize
|
107
|
-
@callbacks = { on_data: [], on_overflow: [] }
|
108
|
-
end
|
178
|
+
@data_mutex.synchronize { @callbacks = { on_data: [], on_overflow: [], on_end_of_snapshot: [] } }
|
109
179
|
end
|
110
180
|
|
111
181
|
# Returns a copy of the current data of one of this subscription's items.
|
@@ -117,9 +187,20 @@ module Lightstreamer
|
|
117
187
|
index = @items.index item_name
|
118
188
|
raise ArgumentError, 'Unknown item' unless index
|
119
189
|
|
120
|
-
@data_mutex.synchronize
|
121
|
-
|
122
|
-
|
190
|
+
@data_mutex.synchronize { @data[index].dup }
|
191
|
+
end
|
192
|
+
|
193
|
+
# Sets the current data for the item with the specified name.
|
194
|
+
#
|
195
|
+
# @param [String] item_name The name of the item to set the data for.
|
196
|
+
# @param [Hash] item_data The new data for the item.
|
197
|
+
def set_item_data(item_name, item_data)
|
198
|
+
index = @items.index item_name
|
199
|
+
raise ArgumentError, 'Unknown item' unless index
|
200
|
+
|
201
|
+
raise ArgumentError, 'Item data must be a hash' unless item_data.is_a? Hash
|
202
|
+
|
203
|
+
@data_mutex.synchronize { @data[index] = item_data.dup }
|
123
204
|
end
|
124
205
|
|
125
206
|
# Processes a line of stream data if it is relevant to this subscription. This method is thread-safe and is intended
|
@@ -132,30 +213,23 @@ module Lightstreamer
|
|
132
213
|
#
|
133
214
|
# @private
|
134
215
|
def process_stream_data(line)
|
135
|
-
process_update_message(line
|
136
|
-
|
137
|
-
|
138
|
-
# Returns the next unique subscription ID.
|
139
|
-
#
|
140
|
-
# @return [Fixnum]
|
141
|
-
#
|
142
|
-
# @private
|
143
|
-
def self.next_id
|
144
|
-
@next_id ||= 0
|
145
|
-
@next_id += 1
|
216
|
+
return true if process_update_message UpdateMessage.parse(line, id, items, fields)
|
217
|
+
return true if process_overflow_message OverflowMessage.parse(line, id, items)
|
218
|
+
return true if process_end_of_snapshot_message EndOfSnapshotMessage.parse(line, id, items)
|
146
219
|
end
|
147
220
|
|
148
221
|
private
|
149
222
|
|
150
|
-
|
151
|
-
update_message = UpdateMessage.parse line, id, items, fields
|
152
|
-
return unless update_message
|
223
|
+
ID_GENERATOR = (1..Float::INFINITY).each
|
153
224
|
|
154
|
-
|
155
|
-
|
156
|
-
|
225
|
+
def sanitize_frequency(frequency)
|
226
|
+
frequency.to_s == 'unfiltered' ? :unfiltered : frequency.to_f
|
227
|
+
end
|
157
228
|
|
158
|
-
|
229
|
+
def process_update_message(message)
|
230
|
+
return unless message
|
231
|
+
|
232
|
+
@data_mutex.synchronize { process_new_values message.item_index, message.values }
|
159
233
|
end
|
160
234
|
|
161
235
|
def process_new_values(item_index, new_values)
|
@@ -167,21 +241,22 @@ module Lightstreamer
|
|
167
241
|
run_callbacks :on_data, @items[item_index], data, new_values
|
168
242
|
end
|
169
243
|
|
170
|
-
def process_overflow_message(
|
171
|
-
|
172
|
-
return unless overflow_message
|
244
|
+
def process_overflow_message(message)
|
245
|
+
return unless message
|
173
246
|
|
174
|
-
@data_mutex.synchronize
|
175
|
-
|
176
|
-
end
|
247
|
+
@data_mutex.synchronize { run_callbacks :on_overflow, @items[message.item_index], message.overflow_size }
|
248
|
+
end
|
177
249
|
|
178
|
-
|
250
|
+
def process_end_of_snapshot_message(message)
|
251
|
+
return unless message
|
252
|
+
|
253
|
+
@data_mutex.synchronize { run_callbacks :on_end_of_snapshot, @items[message.item_index] }
|
179
254
|
end
|
180
255
|
|
181
|
-
def run_callbacks(callback_type, *
|
182
|
-
@callbacks.fetch(callback_type).each
|
183
|
-
|
184
|
-
|
256
|
+
def run_callbacks(callback_type, *args)
|
257
|
+
@callbacks.fetch(callback_type).each { |callback| callback.call self, *args }
|
258
|
+
|
259
|
+
true
|
185
260
|
end
|
186
261
|
end
|
187
262
|
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.
|
4
|
+
version: '0.7'
|
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-
|
11
|
+
date: 2016-07-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: excon
|
@@ -142,14 +142,14 @@ dependencies:
|
|
142
142
|
requirements:
|
143
143
|
- - "~>"
|
144
144
|
- !ruby/object:Gem::Version
|
145
|
-
version: '0.
|
145
|
+
version: '0.42'
|
146
146
|
type: :development
|
147
147
|
prerelease: false
|
148
148
|
version_requirements: !ruby/object:Gem::Requirement
|
149
149
|
requirements:
|
150
150
|
- - "~>"
|
151
151
|
- !ruby/object:Gem::Version
|
152
|
-
version: '0.
|
152
|
+
version: '0.42'
|
153
153
|
- !ruby/object:Gem::Dependency
|
154
154
|
name: yard
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
@@ -178,12 +178,14 @@ files:
|
|
178
178
|
- lib/lightstreamer.rb
|
179
179
|
- lib/lightstreamer/cli/commands/stream_command.rb
|
180
180
|
- lib/lightstreamer/cli/main.rb
|
181
|
-
- lib/lightstreamer/control_connection.rb
|
182
181
|
- lib/lightstreamer/errors.rb
|
183
|
-
- lib/lightstreamer/
|
182
|
+
- lib/lightstreamer/messages/end_of_snapshot_message.rb
|
184
183
|
- lib/lightstreamer/messages/overflow_message.rb
|
184
|
+
- lib/lightstreamer/messages/send_message_outcome_message.rb
|
185
185
|
- lib/lightstreamer/messages/update_message.rb
|
186
|
+
- lib/lightstreamer/post_request.rb
|
186
187
|
- lib/lightstreamer/session.rb
|
188
|
+
- lib/lightstreamer/stream_buffer.rb
|
187
189
|
- lib/lightstreamer/stream_connection.rb
|
188
190
|
- lib/lightstreamer/stream_connection_header.rb
|
189
191
|
- lib/lightstreamer/subscription.rb
|
@@ -1,106 +0,0 @@
|
|
1
|
-
module Lightstreamer
|
2
|
-
# Helper class used by {Session} and is responsible for sending Lightstreamer control requests.
|
3
|
-
#
|
4
|
-
# @private
|
5
|
-
class ControlConnection
|
6
|
-
# Initializes this class for sending Lightstreamer control requests using the specified session ID and control
|
7
|
-
# address.
|
8
|
-
#
|
9
|
-
# @param [String] session_id The Lightstreamer session ID.
|
10
|
-
# @param [String] control_url The URL of the server to send Lightstreamer control requests to.
|
11
|
-
def initialize(session_id, control_url)
|
12
|
-
@session_id = session_id
|
13
|
-
@control_url = URI.join(control_url, '/lightstreamer/control.txt').to_s
|
14
|
-
end
|
15
|
-
|
16
|
-
# Sends a Lightstreamer control request that executes the specified operation with the specified options. If an
|
17
|
-
# error occurs then a {LightstreamerError} subclass will be raised.
|
18
|
-
#
|
19
|
-
# @param [String] operation The operation to execute.
|
20
|
-
# @param [Hash] options The options to include on the request.
|
21
|
-
def execute(operation, options = {})
|
22
|
-
result = execute_post_request build_payload(operation, options)
|
23
|
-
|
24
|
-
raise Errors::SyncError if result.first == 'SYNC ERROR'
|
25
|
-
raise LightstreamerError.build(result[2], result[1]) if result.first != 'OK'
|
26
|
-
end
|
27
|
-
|
28
|
-
# Sends a Lightstreamer subscription control request with the specified operation, table, and options. If an error
|
29
|
-
# occurs then a {LightstreamerError} subclass will be raised.
|
30
|
-
#
|
31
|
-
# @param [:add, :add_silent, :start, :delete] operation The operation to execute.
|
32
|
-
# @param [Fixnum] table The ID of the table this request pertains to.
|
33
|
-
# @param [Hash] options The subscription control request options.
|
34
|
-
def subscription_execute(operation, table, options = {})
|
35
|
-
options[:table] = table
|
36
|
-
|
37
|
-
validate_subscription_options operation, options
|
38
|
-
|
39
|
-
execute operation, options
|
40
|
-
end
|
41
|
-
|
42
|
-
private
|
43
|
-
|
44
|
-
# Validates the passed subscription control request options.
|
45
|
-
def validate_subscription_options(operation, options)
|
46
|
-
raise ArgumentError, 'Invalid table' unless options[:table].is_a? Fixnum
|
47
|
-
raise ArgumentError, 'Unsupported operation' unless [:add, :add_silent, :start, :delete].include? operation
|
48
|
-
|
49
|
-
validate_add_subscription_options options if [:add, :add_silent].include? operation
|
50
|
-
end
|
51
|
-
|
52
|
-
# Validates options required for subscription control requests that perform `add` operations.
|
53
|
-
def validate_add_subscription_options(options)
|
54
|
-
raise ArgumentError, 'Items not specified' if Array(options[:items]).empty?
|
55
|
-
raise ArgumentError, 'Fields not specified' if Array(options[:fields]).empty?
|
56
|
-
raise ArgumentError, 'Unsupported mode' unless [:distinct, :merge].include? options[:mode]
|
57
|
-
end
|
58
|
-
|
59
|
-
# Executes a POST request to the control address with the specified payload. Raises {ConnectionError} if the HTTP
|
60
|
-
# request fails. Returns the response body split into individual lines.
|
61
|
-
def execute_post_request(payload)
|
62
|
-
response = Excon.post @control_url, body: URI.encode_www_form(payload), connect_timeout: 15
|
63
|
-
|
64
|
-
response.body.split("\n").map(&:strip)
|
65
|
-
rescue Excon::Error => error
|
66
|
-
raise Errors::ConnectionError, error.message
|
67
|
-
end
|
68
|
-
|
69
|
-
# Constructs the payload for a Lightstreamer control request based on the given options hash. See {#execute} for
|
70
|
-
# details on the supported keys.
|
71
|
-
def build_payload(operation, options)
|
72
|
-
params = {}
|
73
|
-
|
74
|
-
build_optional_payload_fields options, params
|
75
|
-
|
76
|
-
params[:LS_session] = @session_id
|
77
|
-
params[:LS_op] = operation
|
78
|
-
|
79
|
-
params
|
80
|
-
end
|
81
|
-
|
82
|
-
OPTION_NAME_TO_API_PARAMETER = {
|
83
|
-
table: :LS_table,
|
84
|
-
adapter: :LS_data_adapter,
|
85
|
-
items: :LS_id,
|
86
|
-
fields: :LS_schema,
|
87
|
-
selector: :LS_selector,
|
88
|
-
maximum_update_frequency: :LS_requested_max_frequency
|
89
|
-
}.freeze
|
90
|
-
|
91
|
-
def build_optional_payload_fields(options, params)
|
92
|
-
params[:LS_mode] = options[:mode].to_s.upcase if options[:mode]
|
93
|
-
|
94
|
-
options.each do |key, value|
|
95
|
-
next if key == :mode
|
96
|
-
next if value.nil?
|
97
|
-
|
98
|
-
value = value.map(&:to_s).join(' ') if value.is_a? Array
|
99
|
-
|
100
|
-
api_parameter = OPTION_NAME_TO_API_PARAMETER.fetch key
|
101
|
-
|
102
|
-
params[api_parameter] = value
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|