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.
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