lightstreamer 0.6 → 0.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|