volt 0.9.3.pre1 → 0.9.3.pre2
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/.ruby-version +1 -1
- data/CHANGELOG.md +15 -1
- data/Gemfile +30 -3
- data/README.md +7 -2
- data/Rakefile +17 -0
- data/app/volt/models/user.rb +1 -1
- data/app/volt/tasks/live_query/live_query.rb +0 -1
- data/app/volt/tasks/live_query/live_query_pool.rb +8 -2
- data/app/volt/tasks/live_query/query_tracker.rb +2 -2
- data/app/volt/tasks/query_tasks.rb +10 -27
- data/app/volt/tasks/store_tasks.rb +6 -5
- data/app/volt/tasks/user_tasks.rb +2 -2
- data/docs/UPGRADE_GUIDE.md +14 -0
- data/lib/volt/boot.rb +1 -0
- data/lib/volt/cli/asset_compile.rb +25 -7
- data/lib/volt/cli/console.rb +6 -5
- data/lib/volt/cli/generate.rb +2 -2
- data/lib/volt/config.rb +2 -1
- data/lib/volt/controllers/http_controller.rb +4 -3
- data/lib/volt/controllers/model_controller.rb +41 -19
- data/lib/volt/controllers/template_helpers.rb +19 -0
- data/lib/volt/extra_core/array.rb +6 -0
- data/lib/volt/extra_core/hash.rb +8 -26
- data/lib/volt/extra_core/string.rb +1 -1
- data/lib/volt/models/array_model.rb +12 -4
- data/lib/volt/models/associations.rb +11 -13
- data/lib/volt/models/buffer.rb +1 -1
- data/lib/volt/models/model.rb +22 -13
- data/lib/volt/models/model_helpers/model_change_helpers.rb +0 -1
- data/lib/volt/models/model_helpers/model_helpers.rb +11 -0
- data/lib/volt/models/permissions.rb +9 -12
- data/lib/volt/models/persistors/array_store.rb +7 -7
- data/lib/volt/models/persistors/base.rb +9 -0
- data/lib/volt/models/persistors/cookies.rb +0 -4
- data/lib/volt/models/persistors/flash.rb +0 -4
- data/lib/volt/models/persistors/local_store.rb +0 -4
- data/lib/volt/models/persistors/model_store.rb +13 -21
- data/lib/volt/models/persistors/page.rb +22 -0
- data/lib/volt/models/persistors/params.rb +0 -4
- data/lib/volt/models/persistors/query/query_listener.rb +3 -2
- data/lib/volt/models/url.rb +2 -2
- data/lib/volt/models/validators/unique_validator.rb +1 -1
- data/lib/volt/models.rb +1 -0
- data/lib/volt/page/bindings/attribute_binding.rb +2 -2
- data/lib/volt/page/bindings/base_binding.rb +7 -3
- data/lib/volt/page/bindings/bindings.rb +9 -0
- data/lib/volt/page/bindings/content_binding.rb +2 -2
- data/lib/volt/page/bindings/each_binding.rb +16 -12
- data/lib/volt/page/bindings/event_binding.rb +4 -4
- data/lib/volt/page/bindings/if_binding.rb +3 -3
- data/lib/volt/page/bindings/view_binding.rb +4 -4
- data/lib/volt/page/bindings/yield_binding.rb +3 -3
- data/lib/volt/page/channel.rb +6 -0
- data/lib/volt/page/channel_stub.rb +1 -1
- data/lib/volt/page/page.rb +20 -54
- data/lib/volt/page/path_string_renderer.rb +5 -6
- data/lib/volt/page/string_template_renderer.rb +2 -2
- data/lib/volt/page/targets/attribute_section.rb +47 -0
- data/lib/volt/page/targets/base_section.rb +5 -5
- data/lib/volt/page/targets/binding_document/component_node.rb +6 -1
- data/lib/volt/page/template_renderer.rb +4 -4
- data/lib/volt/reactive/computation.rb +32 -3
- data/lib/volt/router/routes.rb +5 -5
- data/lib/volt/server/component_templates.rb +30 -2
- data/lib/volt/server/forking_server.rb +2 -2
- data/lib/volt/server/message_bus/base_message_bus.rb +52 -0
- data/lib/volt/server/message_bus/message_encoder.rb +64 -0
- data/lib/volt/server/message_bus/peer_to_peer/peer_connection.rb +186 -0
- data/lib/volt/server/message_bus/peer_to_peer/peer_server.rb +78 -0
- data/lib/volt/server/message_bus/peer_to_peer/server_tracker.rb +57 -0
- data/lib/volt/server/message_bus/peer_to_peer/socket_with_timeout.rb +27 -0
- data/lib/volt/server/message_bus/peer_to_peer.rb +198 -0
- data/lib/volt/server/message_bus/redis.rb +1 -0
- data/lib/volt/server/rack/asset_files.rb +2 -2
- data/lib/volt/server/rack/component_paths.rb +1 -1
- data/lib/volt/server/rack/http_resource.rb +3 -2
- data/lib/volt/server/rack/opal_files.rb +6 -9
- data/lib/volt/server/websocket/websocket_handler.rb +0 -3
- data/lib/volt/server.rb +5 -3
- data/lib/volt/spec/setup.rb +11 -12
- data/lib/volt/tasks/dispatcher.rb +8 -12
- data/lib/volt/tasks/task_handler.rb +3 -2
- data/lib/volt/utils/csso_patch.rb +24 -0
- data/lib/volt/utils/promise_patch.rb +2 -0
- data/lib/volt/version.rb +1 -1
- data/lib/volt/volt/app.rb +73 -36
- data/lib/volt/volt/server_setup/app.rb +81 -0
- data/lib/volt.rb +22 -1
- data/spec/apps/kitchen_sink/Gemfile +1 -1
- data/spec/controllers/http_controller_spec.rb +5 -3
- data/spec/controllers/model_controller_spec.rb +2 -2
- data/spec/extra_core/hash_spec.rb +9 -0
- data/spec/integration/list_spec.rb +3 -3
- data/spec/models/associations_spec.rb +10 -2
- data/spec/models/dirty_spec.rb +7 -7
- data/spec/models/model_spec.rb +10 -2
- data/spec/models/permissions_spec.rb +9 -0
- data/spec/models/persistors/store_spec.rb +8 -0
- data/spec/page/bindings/content_binding_spec.rb +6 -2
- data/spec/page/bindings/each_binding_spec.rb +59 -0
- data/spec/page/bindings/if_binding_spec.rb +57 -0
- data/spec/page/path_string_renderer_spec.rb +5 -5
- data/spec/reactive/computation_spec.rb +65 -1
- data/spec/router/routes_spec.rb +1 -1
- data/spec/server/html_parser/sandlebars_parser_spec.rb +12 -22
- data/spec/server/message_bus/message_encoder_spec.rb +49 -0
- data/spec/server/message_bus/peer_to_peer/peer_connection_spec.rb +108 -0
- data/spec/server/message_bus/peer_to_peer/peer_server_spec.rb +66 -0
- data/spec/server/message_bus/peer_to_peer/socket_with_timeout_spec.rb +11 -0
- data/spec/server/message_bus/peer_to_peer_spec.rb +11 -0
- data/spec/server/rack/asset_files_spec.rb +1 -1
- data/spec/server/rack/http_resource_spec.rb +4 -4
- data/spec/spec_helper.rb +16 -3
- data/spec/tasks/dispatcher_spec.rb +17 -5
- data/spec/tasks/live_query_spec.rb +1 -1
- data/spec/tasks/query_tracker_spec.rb +34 -34
- data/spec/tasks/user_tasks_spec.rb +4 -2
- data/templates/project/Gemfile.tt +14 -3
- data/templates/project/config/app.rb.tt +27 -2
- data/volt.gemspec +3 -8
- metadata +32 -101
- data/docs/FAQ.md +0 -7
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Volt supports the concept of a message bus, a bus provides a pub/sub interface
|
|
2
|
+
# to any other volt instance (server, console, runner, etc..) inside a volt
|
|
3
|
+
# cluster. Volt ships with a PeerToPeer message bus out of the box, but you
|
|
4
|
+
# can create or use other message bus's.
|
|
5
|
+
#
|
|
6
|
+
# MessageBus instances inherit from MessageBus::BaseMessageBus and provide
|
|
7
|
+
# two methods 'publish' and 'subscribe'. They should be inside of
|
|
8
|
+
# Volt::MessageBus.
|
|
9
|
+
#
|
|
10
|
+
# publish should take a channel name and a message and deliver the message to
|
|
11
|
+
# any subscried listeners.
|
|
12
|
+
#
|
|
13
|
+
# subscribe should take a channel name and a block. It should yield a message
|
|
14
|
+
# to the block if a message is published to the channel.
|
|
15
|
+
#
|
|
16
|
+
# The implementation details of the pub/sub connection are left to the
|
|
17
|
+
# implemntation. If the user needs to configure server addresses, Volt.config
|
|
18
|
+
# is the prefered location, so it can be configured from config/app.rb
|
|
19
|
+
#
|
|
20
|
+
# MessageBus's should process their messages in their own thread. (And
|
|
21
|
+
# optionally may use a thread pool.)
|
|
22
|
+
#
|
|
23
|
+
# You can use lib/volt/server/message_bus/message_encoder.rb for encoding and
|
|
24
|
+
# encryption if needed.
|
|
25
|
+
#
|
|
26
|
+
# See lib/volt/server/message_bus/peer_to_peer.rb for details on volt's built-in
|
|
27
|
+
# message bus implementation.
|
|
28
|
+
#
|
|
29
|
+
# NOTE: in the future, we plan to add support for round robbin message receiving
|
|
30
|
+
# and other patterns.
|
|
31
|
+
|
|
32
|
+
module Volt
|
|
33
|
+
module MessageBus
|
|
34
|
+
class BaseMessageBus
|
|
35
|
+
# MessagesBus's should take an instance of a Volt::App
|
|
36
|
+
def initialize(volt_app)
|
|
37
|
+
raise "Not implemented"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Subscribe should return an object that you can call .remove on to stop
|
|
41
|
+
# the subscription.
|
|
42
|
+
def subscribe(channel_name, &block)
|
|
43
|
+
raise "Not implemented"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# publish should push out to all subscribed within the volt cluster.
|
|
47
|
+
def publish(channel_name, message)
|
|
48
|
+
raise "Not implemented"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# The message encoder handles reading/writing the message to/from the socket.
|
|
2
|
+
# This includes encrypting and formatting.
|
|
3
|
+
module Volt
|
|
4
|
+
module MessageBus
|
|
5
|
+
class MessageEncoder
|
|
6
|
+
attr_reader :encrypted
|
|
7
|
+
def initialize
|
|
8
|
+
# Message bus is encrypted by default
|
|
9
|
+
@encrypted = (Volt.config.message_bus.try(:disable_encryption) != true)
|
|
10
|
+
|
|
11
|
+
if @encrypted
|
|
12
|
+
# Setup a RbNaCl simple box for handling encryption
|
|
13
|
+
require 'base64'
|
|
14
|
+
begin
|
|
15
|
+
require 'rbnacl/libsodium'
|
|
16
|
+
rescue LoadError => e
|
|
17
|
+
# Ignore, incase they have libsodium installed locally
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
begin
|
|
21
|
+
require 'rbnacl'
|
|
22
|
+
rescue LoadError => e
|
|
23
|
+
Volt.logger.error('Volt requires the rbnacl gem to enable encryption on the message bus. Add it to the gemfile (and rbnacl-sodium if you don\'t have libsodium installed locally')
|
|
24
|
+
raise e
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# use the first 32 chars of the app secret for the encryption key.
|
|
28
|
+
key = Base64.decode64(Volt.config.app_secret)[0..31]
|
|
29
|
+
|
|
30
|
+
@encrypt_box = RbNaCl::SimpleBox.from_secret_key(key)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def encrypt(message)
|
|
35
|
+
if @encrypted
|
|
36
|
+
@encrypt_box.encrypt(message)
|
|
37
|
+
else
|
|
38
|
+
message
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def decrypt(message)
|
|
43
|
+
if @encrypted
|
|
44
|
+
@encrypt_box.decrypt(message)
|
|
45
|
+
else
|
|
46
|
+
message
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def send_message(io, message)
|
|
51
|
+
Marshal.dump(encrypt(message), io)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def receive_message(io)
|
|
55
|
+
begin
|
|
56
|
+
decrypt(Marshal.load(io))
|
|
57
|
+
rescue EOFError => e
|
|
58
|
+
# We get EOFError when the connection closes, return nil
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# PeerConnection manages the connection to a peer, it takes a socket and
|
|
2
|
+
# optionally the ip and port it connected to. If ip and port are given, it
|
|
3
|
+
# will try to reconnect until the server is marked as dead (as checked by
|
|
4
|
+
# message_bus.still_alive?)
|
|
5
|
+
|
|
6
|
+
require 'thread'
|
|
7
|
+
require 'volt/server/message_bus/peer_to_peer/socket_with_timeout'
|
|
8
|
+
require 'volt/server/message_bus/message_encoder'
|
|
9
|
+
|
|
10
|
+
module Volt
|
|
11
|
+
module MessageBus
|
|
12
|
+
class PeerConnection
|
|
13
|
+
CONNECT_TIMEOUT = 2
|
|
14
|
+
# The server id for the connected server
|
|
15
|
+
attr_reader :peer_server_id, :socket
|
|
16
|
+
|
|
17
|
+
def initialize(socket, ip, port, message_bus, server=false)
|
|
18
|
+
@message_bus = message_bus
|
|
19
|
+
@ip = ip
|
|
20
|
+
@port = port
|
|
21
|
+
@server = server
|
|
22
|
+
@socket = socket
|
|
23
|
+
@server_id = message_bus.server_id
|
|
24
|
+
@message_queue = SizedQueue.new(500)
|
|
25
|
+
@reconnect_mutex = Mutex.new
|
|
26
|
+
|
|
27
|
+
# The encoder handles things like formatting and encryption
|
|
28
|
+
@message_encoder = MessageEncoder.new
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
failed = false
|
|
32
|
+
begin
|
|
33
|
+
if server
|
|
34
|
+
# Wait for announcement
|
|
35
|
+
@peer_server_id = @message_encoder.receive_message(@socket)
|
|
36
|
+
@message_encoder.send_message(@socket, @server_id)
|
|
37
|
+
else
|
|
38
|
+
# Announce
|
|
39
|
+
@message_encoder.send_message(@socket, @server_id)
|
|
40
|
+
@peer_server_id = @message_encoder.receive_message(@socket)
|
|
41
|
+
end
|
|
42
|
+
rescue IOError => e
|
|
43
|
+
failed = true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Make sure we aren't already connected
|
|
47
|
+
@message_bus.remove_duplicate_connections
|
|
48
|
+
|
|
49
|
+
# Don't connect to self
|
|
50
|
+
if @failed || @peer_server_id == @server_id
|
|
51
|
+
# Close the connection
|
|
52
|
+
close
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
@listen_thread = Thread.new do
|
|
57
|
+
# Listen for messages in a new thread
|
|
58
|
+
listen
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@worker_thread = Thread.new do
|
|
62
|
+
run_worker
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Close the socket, kill worker threads, and remove from
|
|
67
|
+
# message_bus's peer_connections
|
|
68
|
+
def disconnect
|
|
69
|
+
@disconnected = true
|
|
70
|
+
@message_queue.push(:QUIT)
|
|
71
|
+
begin
|
|
72
|
+
@socket.close
|
|
73
|
+
rescue => e
|
|
74
|
+
# Ignore close error, since we may not be connected
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
@listen_thread.kill
|
|
78
|
+
@worker_thread.kill
|
|
79
|
+
|
|
80
|
+
@message_bus.remove_peer_connection(self)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def publish(message)
|
|
84
|
+
@message_queue.push(message)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def run_worker
|
|
88
|
+
while (message = @message_queue.pop)
|
|
89
|
+
break if message == :QUIT
|
|
90
|
+
|
|
91
|
+
begin
|
|
92
|
+
@message_encoder.send_message(@socket, message)
|
|
93
|
+
# 'Error: closed stream' comes in sometimes
|
|
94
|
+
rescue Errno::ECONNREFUSED, Errno::EPIPE, Error => e
|
|
95
|
+
if reconnect!
|
|
96
|
+
retry
|
|
97
|
+
else
|
|
98
|
+
# Unable to reconnect, die
|
|
99
|
+
break
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def listen
|
|
107
|
+
loop do
|
|
108
|
+
begin
|
|
109
|
+
while (message = @message_encoder.receive_message(@socket))
|
|
110
|
+
# puts "Message: #{message.inspect}"
|
|
111
|
+
break if @disconnected
|
|
112
|
+
@message_bus.handle_message(message)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Got nil from socket
|
|
116
|
+
rescue Errno::ECONNRESET, Errno::EPIPE, IOError => e
|
|
117
|
+
# handle below
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if !@disconnected && !@server
|
|
121
|
+
# Connection was dropped, try to reconnect
|
|
122
|
+
connected = reconnect!
|
|
123
|
+
|
|
124
|
+
# Couldn't reconnect, die
|
|
125
|
+
break unless connected
|
|
126
|
+
else
|
|
127
|
+
break
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# Because servers can have many ips, we try the various ip's until we are
|
|
134
|
+
# able to connect to one.
|
|
135
|
+
# @param [Array] an array of ip strings
|
|
136
|
+
def self.connect_to(message_bus, ips, port)
|
|
137
|
+
ips.split(',').each do |ip|
|
|
138
|
+
begin
|
|
139
|
+
socket = SocketWithTimeout.new(ip, port, CONNECT_TIMEOUT)
|
|
140
|
+
return PeerConnection.new(socket, ip, port, message_bus)
|
|
141
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
|
|
142
|
+
# Unable to connect, next
|
|
143
|
+
next
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
return false
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def still_alive?
|
|
153
|
+
@message_bus.still_alive?(@peer_server_id)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def reconnect!
|
|
157
|
+
# Don't reconnect on the server instances
|
|
158
|
+
return false if @server
|
|
159
|
+
@reconnect_mutex.synchronize do
|
|
160
|
+
loop do
|
|
161
|
+
# Server is no longer reporting as alive, give up on reconnecting
|
|
162
|
+
unless still_alive?
|
|
163
|
+
# Unable to connect, let peer connection die
|
|
164
|
+
disconnect
|
|
165
|
+
return false
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
failed = false
|
|
169
|
+
begin
|
|
170
|
+
socket = SocketWithTimeout.new(@ip, @port, CONNECT_TIMEOUT)
|
|
171
|
+
rescue Errno::ECONNREFUSED, SocketError => e
|
|
172
|
+
# Unable to cnnect, wait 10, try again
|
|
173
|
+
sleep 10
|
|
174
|
+
failed = true
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
unless failed
|
|
178
|
+
# Reconnected
|
|
179
|
+
return true
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# The peer server opens a socket on a port that other instances (volt server,
|
|
2
|
+
# runner, etc...)
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'thread'
|
|
5
|
+
require 'volt/server/message_bus/peer_to_peer/peer_connection'
|
|
6
|
+
|
|
7
|
+
module Volt
|
|
8
|
+
module MessageBus
|
|
9
|
+
class NoAvailablePortException < Exception ; end
|
|
10
|
+
class PeerServer
|
|
11
|
+
def initialize(message_bus)
|
|
12
|
+
@message_bus = message_bus
|
|
13
|
+
|
|
14
|
+
setup_port_ranges
|
|
15
|
+
|
|
16
|
+
ip = Volt.config.message_bus.try(:bind_ip)
|
|
17
|
+
begin
|
|
18
|
+
@server = TCPServer.new(random_port!)
|
|
19
|
+
rescue Errno::EADDRINUSE => e
|
|
20
|
+
# Keep trying ports until we find one that is not in use, or the pool
|
|
21
|
+
# runs out of ports.
|
|
22
|
+
retry
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
run_server
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def run_server
|
|
29
|
+
@main_thread = Thread.new do
|
|
30
|
+
# Start the server
|
|
31
|
+
loop do
|
|
32
|
+
Thread.start(@server.accept) do |socket|
|
|
33
|
+
peer_connection = PeerConnection.new(socket, nil, nil,
|
|
34
|
+
@message_bus, true)
|
|
35
|
+
|
|
36
|
+
@message_bus.add_peer_connection(peer_connection)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def stop
|
|
43
|
+
@main_thread.kill
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def port
|
|
47
|
+
@server.addr[1]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
def setup_port_ranges
|
|
52
|
+
port_ranges = Volt.config.message_bus.try(:bind_port_ranges)
|
|
53
|
+
|
|
54
|
+
if port_ranges
|
|
55
|
+
# Expand any ranges, then sample one from the array
|
|
56
|
+
@ports_pool = port_ranges.to_a.map {|v| v.is_a?(Range) ? v.to_a : v }.flatten
|
|
57
|
+
else
|
|
58
|
+
# port 0, which tells TCPServer to select any random port.
|
|
59
|
+
@ports_pool = [0]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def random_port!
|
|
64
|
+
port = @ports_pool.sample
|
|
65
|
+
|
|
66
|
+
unless port
|
|
67
|
+
# no available ports left
|
|
68
|
+
raise NoAvailablePortException, 'no ports available in Volt.config.message_bus.bind_port_ranges'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# remove from the pool
|
|
72
|
+
@ports_pool.delete(port)
|
|
73
|
+
|
|
74
|
+
port
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# The server tracker uses the database to keep a list of all active servers (or
|
|
2
|
+
# console, runners, etc...). When an server instance starts, it registers with
|
|
3
|
+
# the database, then reads the list of all other active servers.
|
|
4
|
+
|
|
5
|
+
require 'socket'
|
|
6
|
+
|
|
7
|
+
module Volt
|
|
8
|
+
module MessageBus
|
|
9
|
+
class ServerTracker
|
|
10
|
+
UPDATE_INTERVAL = 10
|
|
11
|
+
def initialize(page, server_id, port)
|
|
12
|
+
@page = page
|
|
13
|
+
@server_id = server_id
|
|
14
|
+
@port = port
|
|
15
|
+
|
|
16
|
+
@main_thread = Thread.new do
|
|
17
|
+
# Continually update the database letting the server know the server
|
|
18
|
+
# is active.
|
|
19
|
+
loop do
|
|
20
|
+
begin
|
|
21
|
+
register
|
|
22
|
+
rescue Exception => e
|
|
23
|
+
puts "MessageBus Register Error: #{e.inspect}"
|
|
24
|
+
end
|
|
25
|
+
sleep UPDATE_INTERVAL
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def stop
|
|
31
|
+
@main_thread.kill
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Register this server as active with the database
|
|
35
|
+
def register
|
|
36
|
+
instances = @page.store._active_volt_instances
|
|
37
|
+
instances.where(server_id: @server_id).fetch_first do |item|
|
|
38
|
+
ips = local_ips.join(',')
|
|
39
|
+
time = Time.now.to_i
|
|
40
|
+
if item
|
|
41
|
+
item.assign_attributes(ips: ips, time: time, port: @port)
|
|
42
|
+
else
|
|
43
|
+
instances << {server_id: @server_id, ips: ips, port: @port, time: time}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def local_ips
|
|
49
|
+
addr_infos = Socket.ip_address_list
|
|
50
|
+
|
|
51
|
+
ips = addr_infos.select do |addr|
|
|
52
|
+
addr.pfamily == Socket::PF_INET
|
|
53
|
+
end.map(&:ip_address)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Connect to a socket using the raw timeout, which responds better than the
|
|
2
|
+
# builtin Timeout.
|
|
3
|
+
|
|
4
|
+
require 'socket'
|
|
5
|
+
|
|
6
|
+
module Volt
|
|
7
|
+
class SocketWithTimeout
|
|
8
|
+
def self.new(host, port, timeout=nil)
|
|
9
|
+
if RUBY_PLATFORM == 'java'
|
|
10
|
+
TCPSocket.new(host, port)
|
|
11
|
+
else
|
|
12
|
+
addr = Socket.getaddrinfo(host, nil)
|
|
13
|
+
sock = Socket.new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0)
|
|
14
|
+
|
|
15
|
+
if timeout
|
|
16
|
+
secs = Integer(timeout)
|
|
17
|
+
usecs = Integer((timeout - secs) * 1_000_000)
|
|
18
|
+
optval = [secs, usecs].pack("l_2")
|
|
19
|
+
sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
|
|
20
|
+
sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
|
|
21
|
+
end
|
|
22
|
+
sock.connect(Socket.pack_sockaddr_in(port, addr[0][3]))
|
|
23
|
+
sock
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# The message bus in volt is responsible for passing messages between each of
|
|
2
|
+
# the running app servers (or console, etc...)
|
|
3
|
+
# MessageBus::PeerToPeer is a simple message bus that automatically connects
|
|
4
|
+
# any volt instance using the same database into a cluster. It can then do
|
|
5
|
+
# pub/sub between all instances.
|
|
6
|
+
#
|
|
7
|
+
# How It Works
|
|
8
|
+
# ------------
|
|
9
|
+
# Since the database is assumed to be connected to from each instance, we write
|
|
10
|
+
# a record into the database with the ip of each active server and the current
|
|
11
|
+
# time.
|
|
12
|
+
#
|
|
13
|
+
# When a server connects, it creates a socket connection to all previous servers
|
|
14
|
+
# and announces its self. Messages can then be sent over the socket. Messages
|
|
15
|
+
# are queued
|
|
16
|
+
#
|
|
17
|
+
# Limitations
|
|
18
|
+
# -----------
|
|
19
|
+
#
|
|
20
|
+
# While PeerToPeer should scale fine, it currently uses threads instead of
|
|
21
|
+
# an improved select library (epoll, kqueue, etc...) and non-blocking io. The
|
|
22
|
+
# plan is to rewrite it to use non-blocking io at some point.
|
|
23
|
+
#
|
|
24
|
+
# Also, to simplify the subscription model, messages are sent to all instances
|
|
25
|
+
# reguardless of subscription status. This greatly simplifies the
|
|
26
|
+
# implementation and adds some guarentees in a distributed system, at the cost
|
|
27
|
+
# of messages going to places they don't need to. Since the primary use of the
|
|
28
|
+
# message bus is alerting other instances that data has changed, this limitation
|
|
29
|
+
# is not that big of an issues. (Since the messages are small and usually need
|
|
30
|
+
# to go to most servers) That said, you may want to consider another
|
|
31
|
+
# MessageBus for large scale deployments.
|
|
32
|
+
#
|
|
33
|
+
# The plan also is to improve the way messages are routed and to add other
|
|
34
|
+
# message passing paradigms like round robin receiver.
|
|
35
|
+
require 'thread'
|
|
36
|
+
require 'securerandom'
|
|
37
|
+
|
|
38
|
+
require 'volt/reactive/eventable'
|
|
39
|
+
require 'volt/server/message_bus/peer_to_peer/server_tracker'
|
|
40
|
+
require 'volt/server/message_bus/peer_to_peer/peer_server'
|
|
41
|
+
require 'volt/server/message_bus/peer_to_peer/peer_connection'
|
|
42
|
+
require 'volt/server/message_bus/base_message_bus'
|
|
43
|
+
|
|
44
|
+
# TODO: Right now the message bus uses threads, we should switch it to use a
|
|
45
|
+
# single thread and some form of select:
|
|
46
|
+
# https://practicingruby.com/articles/event-loops-demystified
|
|
47
|
+
|
|
48
|
+
module Volt
|
|
49
|
+
module MessageBus
|
|
50
|
+
class PeerToPeer < BaseMessageBus
|
|
51
|
+
# How long without an update before we mark an instance as dead (in seconds)
|
|
52
|
+
DEAD_TIME = 20
|
|
53
|
+
include Eventable
|
|
54
|
+
|
|
55
|
+
# Use subscribe instead of on provided in Eventable
|
|
56
|
+
alias_method :subscribe, :on
|
|
57
|
+
|
|
58
|
+
attr_reader :server_id, :page
|
|
59
|
+
|
|
60
|
+
def initialize(volt_app)
|
|
61
|
+
# Generate a guid
|
|
62
|
+
@server_id = SecureRandom.uuid
|
|
63
|
+
# The PeerConnection's to peers
|
|
64
|
+
@peer_connections = {}
|
|
65
|
+
# The server id's for each peer we're connected to
|
|
66
|
+
@peer_server_ids = {}
|
|
67
|
+
|
|
68
|
+
@page = volt_app.page
|
|
69
|
+
|
|
70
|
+
setup_peer_server
|
|
71
|
+
start_tracker
|
|
72
|
+
|
|
73
|
+
Thread.new do
|
|
74
|
+
sleep 1
|
|
75
|
+
|
|
76
|
+
connect_to_peers
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The peer server maintains a socket other instances can connect to.
|
|
81
|
+
def setup_peer_server
|
|
82
|
+
@peer_server = PeerServer.new(self)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The tracker updates the socket ip's and port and a timestamp into the
|
|
86
|
+
# database every minute. If the timestamp is more than 2 minutes old,
|
|
87
|
+
# an instance is marked as "dead" and removed.
|
|
88
|
+
def start_tracker
|
|
89
|
+
@server_tracker = ServerTracker.new(page, @server_id, @peer_server.port)
|
|
90
|
+
|
|
91
|
+
# Do the initial registration, and wait until its done before connecting
|
|
92
|
+
# to peers.
|
|
93
|
+
@server_tracker.register()
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def publish(channel, message)
|
|
97
|
+
full_msg = "#{channel}|#{message}"
|
|
98
|
+
@peer_connections.keys.each do |peer|
|
|
99
|
+
begin
|
|
100
|
+
# Queue message on each peer
|
|
101
|
+
peer.publish(full_msg)
|
|
102
|
+
rescue IOError => e
|
|
103
|
+
# Connection to peer lost
|
|
104
|
+
Volt.logger.warn("Message bus connection to peer lost: #{e}")
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Return an array of peer records.
|
|
110
|
+
def peers
|
|
111
|
+
instances = @page.store._active_volt_instances
|
|
112
|
+
|
|
113
|
+
instances.where(server_id: {'$ne' => @server_id}).fetch.sync
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def connect_to_peers
|
|
117
|
+
peers.each do |peer|
|
|
118
|
+
# Start connecting to all at the same time. Since most will connect or
|
|
119
|
+
# timeout, this is the desired behaviour.
|
|
120
|
+
Thread.new do
|
|
121
|
+
peer_connection = PeerConnection.connect_to(self, peer._ips, peer._port)
|
|
122
|
+
|
|
123
|
+
if peer_connection
|
|
124
|
+
add_peer_connection(peer_connection)
|
|
125
|
+
else
|
|
126
|
+
# remove if not alive anymore.
|
|
127
|
+
still_alive?(peer._server_id)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def add_peer_connection(peer_connection)
|
|
134
|
+
@peer_connections[peer_connection] = true
|
|
135
|
+
@peer_server_ids[peer_connection.peer_server_id] = true
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def remove_peer_connection(peer_connection)
|
|
139
|
+
@peer_connections.delete(peer_connection)
|
|
140
|
+
@peer_server_ids.delete(peer_connection.peer_server_id)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Called when a message comes in
|
|
144
|
+
def handle_message(message)
|
|
145
|
+
channel_name, message = message.split('|', 2)
|
|
146
|
+
trigger!(channel_name, message)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# We only want one connection between two instances, this loops through each
|
|
150
|
+
# connection
|
|
151
|
+
def remove_duplicate_connections
|
|
152
|
+
peer_server_ids = {}
|
|
153
|
+
|
|
154
|
+
# remove any we are connected to twice
|
|
155
|
+
@peer_connections.keys.each do |peer|
|
|
156
|
+
peer_id = peer.peer_server_id
|
|
157
|
+
|
|
158
|
+
if peer_id
|
|
159
|
+
# peer is connected
|
|
160
|
+
|
|
161
|
+
if peer_server_ids[peer_id]
|
|
162
|
+
# Peer is already connected somewhere else, remove connection
|
|
163
|
+
peer.disconnect
|
|
164
|
+
|
|
165
|
+
# remove the connection
|
|
166
|
+
@peer_connections.delete(peer)
|
|
167
|
+
else
|
|
168
|
+
# Mark that we are connected
|
|
169
|
+
peer_server_ids[peer_id] = true
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Returns true if the server is still reporting as alive.
|
|
176
|
+
def still_alive?(peer_server_id)
|
|
177
|
+
# Unable to write to the socket, retry until the instance is no
|
|
178
|
+
# longer marking its self as active in the database
|
|
179
|
+
peer_table = @page.store._active_volt_instances
|
|
180
|
+
peer = peer_table.where(server_id: peer_server_id).fetch_first.sync
|
|
181
|
+
if peer
|
|
182
|
+
# Found the peer, retry if it has reported in in the last 2
|
|
183
|
+
# minutes.
|
|
184
|
+
if peer._time > (Time.now.to_i - DEAD_TIME)
|
|
185
|
+
# Peer reported in less than 2 minutes ago
|
|
186
|
+
return true
|
|
187
|
+
else
|
|
188
|
+
# Delete the entry
|
|
189
|
+
peer.destroy
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
false
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require 'volt/server/message_bus/base_message_bus'
|
|
@@ -87,9 +87,9 @@ module Volt
|
|
|
87
87
|
|
|
88
88
|
opal_js_files = []
|
|
89
89
|
if Volt.source_maps?
|
|
90
|
-
opal_js_files += opal_files.environment['volt/
|
|
90
|
+
opal_js_files += opal_files.environment['volt/volt/app'].to_a.map { |v| '/assets/' + v.logical_path + '?body=1' }
|
|
91
91
|
else
|
|
92
|
-
opal_js_files << '/assets/volt/
|
|
92
|
+
opal_js_files << '/assets/volt/volt/app.js'
|
|
93
93
|
end
|
|
94
94
|
opal_js_files << '/components/main.js'
|
|
95
95
|
|
|
@@ -17,7 +17,7 @@ module Volt
|
|
|
17
17
|
# Gem folders with volt in them
|
|
18
18
|
# TODO: we should probably qualify this a bit more
|
|
19
19
|
app_folders += Gem.loaded_specs.values
|
|
20
|
-
.select {|gem| gem.name =~
|
|
20
|
+
.select {|gem| gem.name =~ /^volt/ }
|
|
21
21
|
.map {|gem| "#{gem.full_gem_path}/app" }
|
|
22
22
|
|
|
23
23
|
app_folders.uniq
|