peatio-dao 3.1.3
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 +7 -0
- data/.drone.yml +30 -0
- data/.gitignore +16 -0
- data/.rspec +3 -0
- data/.rubocop.yml +148 -0
- data/.simplecov +17 -0
- data/.travis.yml +18 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +242 -0
- data/README.md +47 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/peatio +12 -0
- data/bin/setup +8 -0
- data/lib/peatio/adapter_registry.rb +25 -0
- data/lib/peatio/auth/error.rb +18 -0
- data/lib/peatio/auth/jwt_authenticator.rb +127 -0
- data/lib/peatio/block.rb +29 -0
- data/lib/peatio/blockchain/abstract.rb +161 -0
- data/lib/peatio/blockchain/error.rb +37 -0
- data/lib/peatio/blockchain/registry.rb +16 -0
- data/lib/peatio/command/base.rb +11 -0
- data/lib/peatio/command/db.rb +20 -0
- data/lib/peatio/command/inject.rb +13 -0
- data/lib/peatio/command/root.rb +14 -0
- data/lib/peatio/command/security.rb +29 -0
- data/lib/peatio/command/service.rb +40 -0
- data/lib/peatio/error.rb +18 -0
- data/lib/peatio/executor.rb +64 -0
- data/lib/peatio/injectors/peatio_events.rb +240 -0
- data/lib/peatio/logger.rb +39 -0
- data/lib/peatio/metrics/server.rb +15 -0
- data/lib/peatio/mq/client.rb +51 -0
- data/lib/peatio/ranger/connection.rb +117 -0
- data/lib/peatio/ranger/events.rb +11 -0
- data/lib/peatio/ranger/router.rb +234 -0
- data/lib/peatio/ranger/web_socket.rb +68 -0
- data/lib/peatio/security/key_generator.rb +26 -0
- data/lib/peatio/sql/client.rb +19 -0
- data/lib/peatio/sql/schema.rb +72 -0
- data/lib/peatio/transaction.rb +137 -0
- data/lib/peatio/upstream/base.rb +116 -0
- data/lib/peatio/upstream/registry.rb +14 -0
- data/lib/peatio/version.rb +3 -0
- data/lib/peatio/wallet/abstract.rb +189 -0
- data/lib/peatio/wallet/error.rb +37 -0
- data/lib/peatio/wallet/registry.rb +16 -0
- data/lib/peatio.rb +47 -0
- data/peatio-dao.gemspec +53 -0
- metadata +455 -0
@@ -0,0 +1,240 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Peatio::Injectors
|
4
|
+
class PeatioEvents
|
5
|
+
attr_accessor :market, :market_name, :base_unit, :quote_unit, :seller_uid, :buyer_uid, :logger
|
6
|
+
|
7
|
+
def run!(exchange_name)
|
8
|
+
require "time"
|
9
|
+
@logger = Peatio::Logger.logger
|
10
|
+
@market = "eurusd"
|
11
|
+
@market_name = "EUR/USD"
|
12
|
+
@base_unit = "eur"
|
13
|
+
@quote_unit = "usd"
|
14
|
+
@seller_uid = 21
|
15
|
+
@buyer_uid = 42
|
16
|
+
@messages = create_messages
|
17
|
+
@opts = {
|
18
|
+
ex_name: exchange_name
|
19
|
+
}
|
20
|
+
EventMachine.run do
|
21
|
+
inject_message()
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def inject_message()
|
26
|
+
if message = @messages.shift
|
27
|
+
type, id, event, data = message
|
28
|
+
Peatio::Ranger::Events.publish(type, id, event, data, @opts)
|
29
|
+
EM.next_tick do
|
30
|
+
inject_message()
|
31
|
+
end
|
32
|
+
else
|
33
|
+
Peatio::MQ::Client.disconnect
|
34
|
+
EventMachine.stop
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def create_messages
|
39
|
+
[
|
40
|
+
public_tickers,
|
41
|
+
public_orderbook,
|
42
|
+
private_order,
|
43
|
+
private_trade_user1,
|
44
|
+
private_trade_user2,
|
45
|
+
public_trade,
|
46
|
+
public_orderbook_increment1,
|
47
|
+
public_orderbook_snapshot1,
|
48
|
+
public_orderbook_increment2,
|
49
|
+
public_orderbook_increment3,
|
50
|
+
]
|
51
|
+
end
|
52
|
+
|
53
|
+
def created_at
|
54
|
+
Time.now - 600
|
55
|
+
end
|
56
|
+
|
57
|
+
def updated_at
|
58
|
+
Time.now
|
59
|
+
end
|
60
|
+
|
61
|
+
alias completed_at updated_at
|
62
|
+
alias canceled_at updated_at
|
63
|
+
|
64
|
+
def public_orderbook
|
65
|
+
[
|
66
|
+
"public",
|
67
|
+
market,
|
68
|
+
"update",
|
69
|
+
{
|
70
|
+
"asks": [
|
71
|
+
["1020.0", "0.005"],
|
72
|
+
["1026.0", "0.03"]
|
73
|
+
],
|
74
|
+
"bids": [
|
75
|
+
["1000.0", "0.25"],
|
76
|
+
["999.0", "0.005"],
|
77
|
+
["994.0", "0.005"],
|
78
|
+
["1.0", "11.0"]
|
79
|
+
]
|
80
|
+
}
|
81
|
+
]
|
82
|
+
end
|
83
|
+
|
84
|
+
def public_orderbook_snapshot1
|
85
|
+
[
|
86
|
+
"public",
|
87
|
+
market,
|
88
|
+
"ob-snap",
|
89
|
+
{
|
90
|
+
"asks": [
|
91
|
+
["1020.0", "0.005"],
|
92
|
+
["1026.0", "0.03"]
|
93
|
+
],
|
94
|
+
"bids": [
|
95
|
+
["1000.0", "0.25"],
|
96
|
+
["999.0", "0.005"],
|
97
|
+
["994.0", "0.005"],
|
98
|
+
["1.0", "11.0"]
|
99
|
+
]
|
100
|
+
}
|
101
|
+
]
|
102
|
+
end
|
103
|
+
|
104
|
+
def public_orderbook_increment1
|
105
|
+
[
|
106
|
+
"public",
|
107
|
+
market,
|
108
|
+
"ob-inc",
|
109
|
+
{
|
110
|
+
"asks": [
|
111
|
+
["1020.0", "0.015"],
|
112
|
+
],
|
113
|
+
}
|
114
|
+
]
|
115
|
+
end
|
116
|
+
|
117
|
+
def public_orderbook_increment2
|
118
|
+
[
|
119
|
+
"public",
|
120
|
+
market,
|
121
|
+
"ob-inc",
|
122
|
+
{
|
123
|
+
"bids": [
|
124
|
+
["1000.0", "0"],
|
125
|
+
],
|
126
|
+
}
|
127
|
+
]
|
128
|
+
end
|
129
|
+
|
130
|
+
def public_orderbook_increment3
|
131
|
+
[
|
132
|
+
"public",
|
133
|
+
market,
|
134
|
+
"ob-inc",
|
135
|
+
{
|
136
|
+
"bids": [
|
137
|
+
["999.0", "0.001"],
|
138
|
+
],
|
139
|
+
}
|
140
|
+
]
|
141
|
+
end
|
142
|
+
|
143
|
+
def public_tickers
|
144
|
+
[
|
145
|
+
"public",
|
146
|
+
"global",
|
147
|
+
"tickers",
|
148
|
+
{
|
149
|
+
market => {
|
150
|
+
"name": market_name,
|
151
|
+
"base_unit": base_unit,
|
152
|
+
"quote_unit": quote_unit,
|
153
|
+
"low": "1000.0",
|
154
|
+
"high": "10000.0",
|
155
|
+
"last": "1000.0",
|
156
|
+
"open": 1000.0,
|
157
|
+
"volume": "0.0",
|
158
|
+
"sell": "1020.0",
|
159
|
+
"buy": "1000.0",
|
160
|
+
"at": Time.now.to_i
|
161
|
+
}
|
162
|
+
}
|
163
|
+
]
|
164
|
+
end
|
165
|
+
|
166
|
+
def private_order
|
167
|
+
[
|
168
|
+
"private",
|
169
|
+
"IDABC0000001",
|
170
|
+
"order",
|
171
|
+
{
|
172
|
+
"id": 22,
|
173
|
+
"at": created_at.to_i,
|
174
|
+
"market": market,
|
175
|
+
"kind": "bid",
|
176
|
+
"price": "1026.0",
|
177
|
+
"state": "wait",
|
178
|
+
"volume": "0.001",
|
179
|
+
"origin_volume": "0.001"
|
180
|
+
}
|
181
|
+
]
|
182
|
+
end
|
183
|
+
|
184
|
+
def private_trade_user1
|
185
|
+
[
|
186
|
+
"private",
|
187
|
+
"IDABC0000001",
|
188
|
+
"trade",
|
189
|
+
{
|
190
|
+
"id": 7,
|
191
|
+
"kind": "ask",
|
192
|
+
"at": created_at.to_i,
|
193
|
+
"price": "1020.0",
|
194
|
+
"volume": "0.001",
|
195
|
+
"ask_id": 15,
|
196
|
+
"bid_id": 22,
|
197
|
+
"market": market
|
198
|
+
}
|
199
|
+
]
|
200
|
+
end
|
201
|
+
|
202
|
+
def private_trade_user2
|
203
|
+
[
|
204
|
+
"private",
|
205
|
+
"IDABC0000002",
|
206
|
+
"trade",
|
207
|
+
{
|
208
|
+
"id": 7,
|
209
|
+
"kind": "bid",
|
210
|
+
"at": created_at.to_i,
|
211
|
+
"price": "1020.0",
|
212
|
+
"volume": "0.001",
|
213
|
+
"ask_id": 15,
|
214
|
+
"bid_id": 22,
|
215
|
+
"market": market
|
216
|
+
}
|
217
|
+
]
|
218
|
+
end
|
219
|
+
|
220
|
+
def public_trade
|
221
|
+
[
|
222
|
+
"public",
|
223
|
+
market,
|
224
|
+
"trades",
|
225
|
+
{
|
226
|
+
"trades": [
|
227
|
+
{
|
228
|
+
"tid": 7,
|
229
|
+
"taker_type": "buy",
|
230
|
+
"date": created_at.to_i,
|
231
|
+
"price": "1020.0",
|
232
|
+
"amount":
|
233
|
+
"0.001"
|
234
|
+
}
|
235
|
+
]
|
236
|
+
}
|
237
|
+
]
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Peatio
|
4
|
+
class Logger
|
5
|
+
class << self
|
6
|
+
def logger
|
7
|
+
@logger ||= ::Logger.new(STDERR, level: level)
|
8
|
+
end
|
9
|
+
|
10
|
+
def level
|
11
|
+
(ENV["LOG_LEVEL"] || "info").downcase.to_sym
|
12
|
+
end
|
13
|
+
|
14
|
+
def debug(progname=nil, &block)
|
15
|
+
logger.debug(progname, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def info(progname=nil, &block)
|
19
|
+
logger.info(progname, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def warn(progname=nil, &block)
|
23
|
+
logger.warn(progname, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def error(progname=nil, &block)
|
27
|
+
logger.error(progname, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def fatal(progname=nil, &block)
|
31
|
+
logger.fatal(progname, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def unknown(progname=nil, &block)
|
35
|
+
logger.unknown(progname, &block)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Peatio::Metrics
|
4
|
+
class Server
|
5
|
+
def self.app(registry)
|
6
|
+
Rack::Builder.new do |builder|
|
7
|
+
builder.use Rack::CommonLogger
|
8
|
+
builder.use Rack::ShowExceptions
|
9
|
+
builder.use Rack::Deflater
|
10
|
+
builder.use Prometheus::Middleware::Exporter, registry: registry
|
11
|
+
builder.run ->(_) { [404, {"Content-Type" => "text/html"}, ["Not found\n"]] }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Peatio::MQ
|
4
|
+
class Client
|
5
|
+
class << self
|
6
|
+
attr_accessor :connection
|
7
|
+
|
8
|
+
def connect!
|
9
|
+
options = {
|
10
|
+
host: ENV["RABBITMQ_HOST"] || "0.0.0.0",
|
11
|
+
port: ENV["RABBITMQ_PORT"] || "5672",
|
12
|
+
username: ENV["RABBITMQ_USER"],
|
13
|
+
password: ENV["RABBITMQ_PASSWORD"],
|
14
|
+
}
|
15
|
+
@connection = Bunny.new(options)
|
16
|
+
@connection.start
|
17
|
+
end
|
18
|
+
|
19
|
+
def disconnect
|
20
|
+
@connection.close
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
Client.connect! unless Peatio::MQ::Client.connection
|
26
|
+
@channel = Client.connection.create_channel
|
27
|
+
@exchanges = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
def exchange(name, type="topic")
|
31
|
+
@exchanges[name] ||= @channel.exchange(name, type: type)
|
32
|
+
end
|
33
|
+
|
34
|
+
def publish(ex_name, type, id, event, payload)
|
35
|
+
routing_key = [type, id, event].join(".")
|
36
|
+
serialized_data = JSON.dump(payload)
|
37
|
+
exchange(ex_name).publish(serialized_data, routing_key: routing_key)
|
38
|
+
Peatio::Logger.debug { "published event to #{routing_key} " }
|
39
|
+
end
|
40
|
+
|
41
|
+
def subscribe(ex_name, &callback)
|
42
|
+
suffix = "#{Socket.gethostname.split(/-/).last}#{Random.rand(10_000)}"
|
43
|
+
queue_name = "ranger.#{suffix}"
|
44
|
+
|
45
|
+
@channel
|
46
|
+
.queue(queue_name, durable: false, auto_delete: true)
|
47
|
+
.bind(exchange(ex_name), routing_key: "#").subscribe(&callback)
|
48
|
+
Peatio::Logger.info "Subscribed to exchange #{ex_name}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Peatio::Ranger
|
2
|
+
class Connection
|
3
|
+
attr_reader :socket, :user, :authorized, :streams, :logger, :id
|
4
|
+
|
5
|
+
def initialize(router, socket, logger)
|
6
|
+
@id = SecureRandom.hex(10)
|
7
|
+
@router = router
|
8
|
+
@socket = socket
|
9
|
+
@logger = logger
|
10
|
+
@user = nil
|
11
|
+
@authorized = false
|
12
|
+
@streams = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def inspect
|
16
|
+
if authorized
|
17
|
+
"<Connection id=#{id} user=#{user}>"
|
18
|
+
else
|
19
|
+
"<Connection id=#{id}>"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def send_raw(payload)
|
24
|
+
if user
|
25
|
+
logger.debug { "sending to user #{user.inspect} payload: #{payload}" }
|
26
|
+
else
|
27
|
+
logger.debug { "sending to anonymous payload: #{payload}" }
|
28
|
+
end
|
29
|
+
@socket.send(payload)
|
30
|
+
end
|
31
|
+
|
32
|
+
def send(method, data)
|
33
|
+
payload = JSON.dump(method => data)
|
34
|
+
send_raw(payload)
|
35
|
+
end
|
36
|
+
|
37
|
+
def authenticate(authenticator, jwt)
|
38
|
+
payload = {}
|
39
|
+
authorized = false
|
40
|
+
begin
|
41
|
+
payload = authenticator.authenticate!(jwt)
|
42
|
+
authorized = true
|
43
|
+
rescue Peatio::Auth::Error => e
|
44
|
+
logger.warn e.message
|
45
|
+
end
|
46
|
+
[authorized, payload]
|
47
|
+
end
|
48
|
+
|
49
|
+
def subscribe(subscribed_streams)
|
50
|
+
raise "Streams must be an array of strings" unless subscribed_streams.is_a?(Array)
|
51
|
+
|
52
|
+
subscribed_streams.each do |stream|
|
53
|
+
stream = stream.to_s
|
54
|
+
next if stream.empty?
|
55
|
+
|
56
|
+
unless @streams[stream]
|
57
|
+
@streams[stream] = true
|
58
|
+
@router.on_subscribe(self, stream)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
send(:success, message: "subscribed", streams: streams.keys)
|
62
|
+
end
|
63
|
+
|
64
|
+
def unsubscribe(unsubscribed_streams)
|
65
|
+
raise "Streams must be an array of strings" unless unsubscribed_streams.is_a?(Array)
|
66
|
+
|
67
|
+
unsubscribed_streams.each do |stream|
|
68
|
+
stream = stream.to_s
|
69
|
+
next if stream.empty?
|
70
|
+
|
71
|
+
if @streams[stream]
|
72
|
+
@streams.delete(stream)
|
73
|
+
@router.on_unsubscribe(self, stream)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
send(:success, message: "unsubscribed", streams: streams.keys)
|
77
|
+
end
|
78
|
+
|
79
|
+
def handle(msg)
|
80
|
+
return if msg.to_s.empty?
|
81
|
+
|
82
|
+
if msg =~ /^ping/
|
83
|
+
send_raw("pong")
|
84
|
+
return
|
85
|
+
end
|
86
|
+
|
87
|
+
data = JSON.parse(msg)
|
88
|
+
case data["event"]
|
89
|
+
when "subscribe"
|
90
|
+
subscribe(data["streams"])
|
91
|
+
when "unsubscribe"
|
92
|
+
unsubscribe(data["streams"])
|
93
|
+
end
|
94
|
+
rescue JSON::ParserError => e
|
95
|
+
logger.debug { "#{e}, msg: `#{msg}`" }
|
96
|
+
end
|
97
|
+
|
98
|
+
def handshake(authenticator, hs)
|
99
|
+
query = URI.decode_www_form(hs.query_string)
|
100
|
+
subscribe(query.map {|item| item.last if item.first == "stream" })
|
101
|
+
logger.debug "WebSocket connection openned"
|
102
|
+
headers = hs.headers_downcased
|
103
|
+
return unless headers.key?("authorization")
|
104
|
+
|
105
|
+
authorized, payload = authenticate(authenticator, headers["authorization"])
|
106
|
+
|
107
|
+
if !authorized
|
108
|
+
logger.debug "Authentication failed for UID:#{payload[:uid]}"
|
109
|
+
raise EM::WebSocket::HandshakeError, "Authorization failed"
|
110
|
+
else
|
111
|
+
@user = payload[:uid]
|
112
|
+
@authorized = true
|
113
|
+
logger.debug "User #{@user} authenticated #{@streams}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Peatio::Ranger
|
4
|
+
class Events
|
5
|
+
def self.publish(type, id, event, payload, opts={})
|
6
|
+
ex_name = opts[:ex_name] || "peatio.events.ranger"
|
7
|
+
@client ||= Peatio::MQ::Client.new
|
8
|
+
@client.publish(ex_name, type, id, event, payload)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,234 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Peatio::Ranger
|
4
|
+
class Router
|
5
|
+
attr_reader :connections
|
6
|
+
attr_reader :connections_by_userid
|
7
|
+
attr_reader :streams_sockets
|
8
|
+
attr_reader :logger
|
9
|
+
|
10
|
+
class ConnectionArray < Array
|
11
|
+
def delete(connection)
|
12
|
+
delete_if do |c|
|
13
|
+
c.id == connection.id
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(prometheus=nil)
|
19
|
+
@connections = {}
|
20
|
+
@connections_by_userid = {}
|
21
|
+
@streams_sockets = {}
|
22
|
+
@logger = Peatio::Logger.logger
|
23
|
+
@stores = {}
|
24
|
+
init_metrics(prometheus)
|
25
|
+
end
|
26
|
+
|
27
|
+
def init_metrics(prometheus)
|
28
|
+
return unless prometheus
|
29
|
+
|
30
|
+
@prometheus = prometheus
|
31
|
+
@metric_connections_total = @prometheus.counter(
|
32
|
+
:ranger_connections_total,
|
33
|
+
docstring: "Total number of connections to ranger from the start",
|
34
|
+
labels: [:auth]
|
35
|
+
)
|
36
|
+
@metric_connections_current = @prometheus.gauge(
|
37
|
+
:ranger_connections_current,
|
38
|
+
docstring: "Current number of connections to ranger",
|
39
|
+
labels: [:auth]
|
40
|
+
)
|
41
|
+
@metric_subscriptions_current = @prometheus.gauge(
|
42
|
+
:ranger_subscriptions_current,
|
43
|
+
docstring: "Current number of streams subscriptions to ranger",
|
44
|
+
labels: [:stream]
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
def snapshot?(stream)
|
49
|
+
stream.end_with?("-snap")
|
50
|
+
end
|
51
|
+
|
52
|
+
def increment?(stream)
|
53
|
+
stream.end_with?("-inc")
|
54
|
+
end
|
55
|
+
|
56
|
+
def storekey(stream)
|
57
|
+
stream.gsub(/-(snap|inc)$/, "")
|
58
|
+
end
|
59
|
+
|
60
|
+
def stats
|
61
|
+
[
|
62
|
+
"==== Metrics ====",
|
63
|
+
"ranger_connections_total{auth=\"public\"}: %d" % [@metric_connections_total.get(labels: {auth: "public"})],
|
64
|
+
"ranger_connections_total{auth=\"private\"}: %d" % [@metric_connections_total.get(labels: {auth: "private"})],
|
65
|
+
"ranger_connections_current{auth=\"public\"}: %d" % [@metric_connections_current.get(labels: {auth: "public"})],
|
66
|
+
"ranger_connections_current{auth=\"private\"}: %d" % [@metric_connections_current.get(labels: {auth: "private"})],
|
67
|
+
"ranger_subscriptions_current: %d" % [compute_streams_subscriptions()],
|
68
|
+
"ranger_streams_kinds: %d" % [compute_streams_kinds()],
|
69
|
+
].join("\n")
|
70
|
+
end
|
71
|
+
|
72
|
+
def debug
|
73
|
+
[
|
74
|
+
"==== Debug ====",
|
75
|
+
"connections: %s" % [@connections.inspect],
|
76
|
+
"connections_by_userid: %s" % [@connections_by_userid],
|
77
|
+
"streams_sockets: %s" % [@streams_sockets],
|
78
|
+
].join("\n")
|
79
|
+
end
|
80
|
+
|
81
|
+
def compute_connections_all
|
82
|
+
@connections.size
|
83
|
+
end
|
84
|
+
|
85
|
+
def compute_connections_private
|
86
|
+
@connections_by_userid.each_value.map(&:size).reduce(0, :+)
|
87
|
+
end
|
88
|
+
|
89
|
+
def compute_stream_subscriptions(stream)
|
90
|
+
@streams_sockets[stream]&.size || 0
|
91
|
+
end
|
92
|
+
|
93
|
+
def compute_streams_subscriptions
|
94
|
+
@streams_sockets.each_value.map(&:size).reduce(0, :+)
|
95
|
+
end
|
96
|
+
|
97
|
+
def compute_streams_kinds
|
98
|
+
@streams_sockets.size
|
99
|
+
end
|
100
|
+
|
101
|
+
def sanity_check_metrics_connections
|
102
|
+
return unless @metric_connections_current
|
103
|
+
|
104
|
+
connections_current_all = @metric_connections_current.values.values.reduce(0, :+)
|
105
|
+
return if connections_current_all == compute_connections_all()
|
106
|
+
|
107
|
+
logger.warn "slip detected in metric_connections_current, recalculating"
|
108
|
+
connections_current_private = compute_connections_private()
|
109
|
+
@metric_connections_current.set(connections_current_private, labels: {auth: "private"})
|
110
|
+
@metric_connections_current.set(compute_connections_all() - connections_current_private, labels: {auth: "public"})
|
111
|
+
end
|
112
|
+
|
113
|
+
def on_connection_open(connection)
|
114
|
+
@connections[connection.id] = connection
|
115
|
+
unless connection.authorized
|
116
|
+
@metric_connections_current&.increment(labels: {auth: "public"})
|
117
|
+
@metric_connections_total&.increment(labels: {auth: "public"})
|
118
|
+
return
|
119
|
+
end
|
120
|
+
@metric_connections_current&.increment(labels: {auth: "private"})
|
121
|
+
@metric_connections_total&.increment(labels: {auth: "private"})
|
122
|
+
|
123
|
+
@connections_by_userid[connection.user] ||= ConnectionArray.new
|
124
|
+
@connections_by_userid[connection.user] << connection
|
125
|
+
end
|
126
|
+
|
127
|
+
def on_connection_close(connection)
|
128
|
+
@connections.delete(connection.id)
|
129
|
+
connection.streams.keys.each do |stream|
|
130
|
+
on_unsubscribe(connection, stream)
|
131
|
+
end
|
132
|
+
|
133
|
+
unless connection.authorized
|
134
|
+
@metric_connections_current&.decrement(labels: {auth: "public"})
|
135
|
+
sanity_check_metrics_connections
|
136
|
+
return
|
137
|
+
end
|
138
|
+
@metric_connections_current&.decrement(labels: {auth: "private"})
|
139
|
+
|
140
|
+
@connections_by_userid[connection.user].delete(connection)
|
141
|
+
@connections_by_userid.delete(connection.user) \
|
142
|
+
if @connections_by_userid[connection.user].empty?
|
143
|
+
sanity_check_metrics_connections
|
144
|
+
end
|
145
|
+
|
146
|
+
def on_subscribe(connection, stream)
|
147
|
+
@streams_sockets[stream] ||= ConnectionArray.new
|
148
|
+
@streams_sockets[stream] << connection
|
149
|
+
send_snapshot_and_increments(connection, storekey(stream)) if increment?(stream)
|
150
|
+
@metric_subscriptions_current&.set(compute_stream_subscriptions(stream), labels: {stream: stream})
|
151
|
+
end
|
152
|
+
|
153
|
+
def send_snapshot_and_increments(connection, key)
|
154
|
+
return unless @stores[key]
|
155
|
+
return unless @stores[key][:snapshot]
|
156
|
+
|
157
|
+
connection.send_raw(@stores[key][:snapshot])
|
158
|
+
@stores[key][:increments]&.each {|inc| connection.send_raw(inc) }
|
159
|
+
end
|
160
|
+
|
161
|
+
def on_unsubscribe(connection, stream)
|
162
|
+
return unless @streams_sockets[stream]
|
163
|
+
|
164
|
+
@streams_sockets[stream].delete(connection)
|
165
|
+
@streams_sockets.delete(stream) if @streams_sockets[stream].empty?
|
166
|
+
@metric_subscriptions_current&.set(compute_stream_subscriptions(stream), labels: {stream: stream})
|
167
|
+
end
|
168
|
+
|
169
|
+
def send_private_message(user_id, event, payload_decoded)
|
170
|
+
Array(@connections_by_userid[user_id]).each do |connection|
|
171
|
+
connection.send(event, payload_decoded) if connection.streams.include?(event)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def send_public_message(stream, raw_message)
|
176
|
+
Array(@streams_sockets[stream]).each do |connection|
|
177
|
+
connection.send_raw(raw_message)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
#
|
182
|
+
# routing key format: type.id.event
|
183
|
+
# * `type` can be *public* or *private*
|
184
|
+
# * `id` can be user id or market id
|
185
|
+
# * `event` is the event identifier, ex: order_completed, trade, ...
|
186
|
+
#
|
187
|
+
def on_message(delivery_info, _metadata, payload)
|
188
|
+
routing_key = delivery_info.routing_key
|
189
|
+
if routing_key.count(".") != 2
|
190
|
+
logger.error { "invalid routing key from amqp: #{routing_key}" }
|
191
|
+
return
|
192
|
+
end
|
193
|
+
|
194
|
+
type, id, event = routing_key.split(".")
|
195
|
+
payload_decoded = JSON.parse(payload)
|
196
|
+
|
197
|
+
if type == "private"
|
198
|
+
send_private_message(id, event, payload_decoded)
|
199
|
+
return
|
200
|
+
end
|
201
|
+
|
202
|
+
stream = [id, event].join(".")
|
203
|
+
message = JSON.dump(stream => payload_decoded)
|
204
|
+
|
205
|
+
if snapshot?(event)
|
206
|
+
key = storekey(stream)
|
207
|
+
|
208
|
+
unless @stores[key]
|
209
|
+
# Send the snapshot to subscribers of -inc stream if there were no snapshot before
|
210
|
+
send_public_message("#{key}-inc", message)
|
211
|
+
end
|
212
|
+
|
213
|
+
@stores[key] = {
|
214
|
+
snapshot: message,
|
215
|
+
increments: [],
|
216
|
+
}
|
217
|
+
return
|
218
|
+
end
|
219
|
+
|
220
|
+
if increment?(event)
|
221
|
+
key = storekey(stream)
|
222
|
+
|
223
|
+
unless @stores[key]
|
224
|
+
logger.warn { "Discard increment received before snapshot for store:#{key}" }
|
225
|
+
return
|
226
|
+
end
|
227
|
+
|
228
|
+
@stores[key][:increments] << message
|
229
|
+
end
|
230
|
+
|
231
|
+
send_public_message(stream, message)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|