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.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +15 -1
  4. data/Gemfile +30 -3
  5. data/README.md +7 -2
  6. data/Rakefile +17 -0
  7. data/app/volt/models/user.rb +1 -1
  8. data/app/volt/tasks/live_query/live_query.rb +0 -1
  9. data/app/volt/tasks/live_query/live_query_pool.rb +8 -2
  10. data/app/volt/tasks/live_query/query_tracker.rb +2 -2
  11. data/app/volt/tasks/query_tasks.rb +10 -27
  12. data/app/volt/tasks/store_tasks.rb +6 -5
  13. data/app/volt/tasks/user_tasks.rb +2 -2
  14. data/docs/UPGRADE_GUIDE.md +14 -0
  15. data/lib/volt/boot.rb +1 -0
  16. data/lib/volt/cli/asset_compile.rb +25 -7
  17. data/lib/volt/cli/console.rb +6 -5
  18. data/lib/volt/cli/generate.rb +2 -2
  19. data/lib/volt/config.rb +2 -1
  20. data/lib/volt/controllers/http_controller.rb +4 -3
  21. data/lib/volt/controllers/model_controller.rb +41 -19
  22. data/lib/volt/controllers/template_helpers.rb +19 -0
  23. data/lib/volt/extra_core/array.rb +6 -0
  24. data/lib/volt/extra_core/hash.rb +8 -26
  25. data/lib/volt/extra_core/string.rb +1 -1
  26. data/lib/volt/models/array_model.rb +12 -4
  27. data/lib/volt/models/associations.rb +11 -13
  28. data/lib/volt/models/buffer.rb +1 -1
  29. data/lib/volt/models/model.rb +22 -13
  30. data/lib/volt/models/model_helpers/model_change_helpers.rb +0 -1
  31. data/lib/volt/models/model_helpers/model_helpers.rb +11 -0
  32. data/lib/volt/models/permissions.rb +9 -12
  33. data/lib/volt/models/persistors/array_store.rb +7 -7
  34. data/lib/volt/models/persistors/base.rb +9 -0
  35. data/lib/volt/models/persistors/cookies.rb +0 -4
  36. data/lib/volt/models/persistors/flash.rb +0 -4
  37. data/lib/volt/models/persistors/local_store.rb +0 -4
  38. data/lib/volt/models/persistors/model_store.rb +13 -21
  39. data/lib/volt/models/persistors/page.rb +22 -0
  40. data/lib/volt/models/persistors/params.rb +0 -4
  41. data/lib/volt/models/persistors/query/query_listener.rb +3 -2
  42. data/lib/volt/models/url.rb +2 -2
  43. data/lib/volt/models/validators/unique_validator.rb +1 -1
  44. data/lib/volt/models.rb +1 -0
  45. data/lib/volt/page/bindings/attribute_binding.rb +2 -2
  46. data/lib/volt/page/bindings/base_binding.rb +7 -3
  47. data/lib/volt/page/bindings/bindings.rb +9 -0
  48. data/lib/volt/page/bindings/content_binding.rb +2 -2
  49. data/lib/volt/page/bindings/each_binding.rb +16 -12
  50. data/lib/volt/page/bindings/event_binding.rb +4 -4
  51. data/lib/volt/page/bindings/if_binding.rb +3 -3
  52. data/lib/volt/page/bindings/view_binding.rb +4 -4
  53. data/lib/volt/page/bindings/yield_binding.rb +3 -3
  54. data/lib/volt/page/channel.rb +6 -0
  55. data/lib/volt/page/channel_stub.rb +1 -1
  56. data/lib/volt/page/page.rb +20 -54
  57. data/lib/volt/page/path_string_renderer.rb +5 -6
  58. data/lib/volt/page/string_template_renderer.rb +2 -2
  59. data/lib/volt/page/targets/attribute_section.rb +47 -0
  60. data/lib/volt/page/targets/base_section.rb +5 -5
  61. data/lib/volt/page/targets/binding_document/component_node.rb +6 -1
  62. data/lib/volt/page/template_renderer.rb +4 -4
  63. data/lib/volt/reactive/computation.rb +32 -3
  64. data/lib/volt/router/routes.rb +5 -5
  65. data/lib/volt/server/component_templates.rb +30 -2
  66. data/lib/volt/server/forking_server.rb +2 -2
  67. data/lib/volt/server/message_bus/base_message_bus.rb +52 -0
  68. data/lib/volt/server/message_bus/message_encoder.rb +64 -0
  69. data/lib/volt/server/message_bus/peer_to_peer/peer_connection.rb +186 -0
  70. data/lib/volt/server/message_bus/peer_to_peer/peer_server.rb +78 -0
  71. data/lib/volt/server/message_bus/peer_to_peer/server_tracker.rb +57 -0
  72. data/lib/volt/server/message_bus/peer_to_peer/socket_with_timeout.rb +27 -0
  73. data/lib/volt/server/message_bus/peer_to_peer.rb +198 -0
  74. data/lib/volt/server/message_bus/redis.rb +1 -0
  75. data/lib/volt/server/rack/asset_files.rb +2 -2
  76. data/lib/volt/server/rack/component_paths.rb +1 -1
  77. data/lib/volt/server/rack/http_resource.rb +3 -2
  78. data/lib/volt/server/rack/opal_files.rb +6 -9
  79. data/lib/volt/server/websocket/websocket_handler.rb +0 -3
  80. data/lib/volt/server.rb +5 -3
  81. data/lib/volt/spec/setup.rb +11 -12
  82. data/lib/volt/tasks/dispatcher.rb +8 -12
  83. data/lib/volt/tasks/task_handler.rb +3 -2
  84. data/lib/volt/utils/csso_patch.rb +24 -0
  85. data/lib/volt/utils/promise_patch.rb +2 -0
  86. data/lib/volt/version.rb +1 -1
  87. data/lib/volt/volt/app.rb +73 -36
  88. data/lib/volt/volt/server_setup/app.rb +81 -0
  89. data/lib/volt.rb +22 -1
  90. data/spec/apps/kitchen_sink/Gemfile +1 -1
  91. data/spec/controllers/http_controller_spec.rb +5 -3
  92. data/spec/controllers/model_controller_spec.rb +2 -2
  93. data/spec/extra_core/hash_spec.rb +9 -0
  94. data/spec/integration/list_spec.rb +3 -3
  95. data/spec/models/associations_spec.rb +10 -2
  96. data/spec/models/dirty_spec.rb +7 -7
  97. data/spec/models/model_spec.rb +10 -2
  98. data/spec/models/permissions_spec.rb +9 -0
  99. data/spec/models/persistors/store_spec.rb +8 -0
  100. data/spec/page/bindings/content_binding_spec.rb +6 -2
  101. data/spec/page/bindings/each_binding_spec.rb +59 -0
  102. data/spec/page/bindings/if_binding_spec.rb +57 -0
  103. data/spec/page/path_string_renderer_spec.rb +5 -5
  104. data/spec/reactive/computation_spec.rb +65 -1
  105. data/spec/router/routes_spec.rb +1 -1
  106. data/spec/server/html_parser/sandlebars_parser_spec.rb +12 -22
  107. data/spec/server/message_bus/message_encoder_spec.rb +49 -0
  108. data/spec/server/message_bus/peer_to_peer/peer_connection_spec.rb +108 -0
  109. data/spec/server/message_bus/peer_to_peer/peer_server_spec.rb +66 -0
  110. data/spec/server/message_bus/peer_to_peer/socket_with_timeout_spec.rb +11 -0
  111. data/spec/server/message_bus/peer_to_peer_spec.rb +11 -0
  112. data/spec/server/rack/asset_files_spec.rb +1 -1
  113. data/spec/server/rack/http_resource_spec.rb +4 -4
  114. data/spec/spec_helper.rb +16 -3
  115. data/spec/tasks/dispatcher_spec.rb +17 -5
  116. data/spec/tasks/live_query_spec.rb +1 -1
  117. data/spec/tasks/query_tracker_spec.rb +34 -34
  118. data/spec/tasks/user_tasks_spec.rb +4 -2
  119. data/templates/project/Gemfile.tt +14 -3
  120. data/templates/project/config/app.rb.tt +27 -2
  121. data/volt.gemspec +3 -8
  122. metadata +32 -101
  123. 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/page/page'].to_a.map { |v| '/assets/' + v.logical_path + '?body=1' }
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/page/page.js'
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 =~ /volt/ }
20
+ .select {|gem| gem.name =~ /^volt/ }
21
21
  .map {|gem| "#{gem.full_gem_path}/app" }
22
22
 
23
23
  app_folders.uniq