volt 0.9.3.pre1 → 0.9.3.pre2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|