distributed_demo 1.0.4 → 1.0.5

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.
data/README ADDED
@@ -0,0 +1,34 @@
1
+ Demo for the talk "Distributed Systems with Rack"
2
+ -------------------------------------------------
3
+
4
+ This is the complete code for my Scottish Ruby Conference talk. It demonstrates the idea
5
+ of having a website generated by multiple distinct and distributed services. For the
6
+ purposes of the demo, we have a server application which assigns roles to computers that
7
+ connect. In reality, you'd probably have a legitimate proxy server sit in front of the
8
+ clusters of nodes...
9
+
10
+ In our case though, we keep a list of clients that are connected, and which roles
11
+ they've been assigned to. When we want to generate a page, we determine which roles are
12
+ necessary, pick one of the clients from each role at random, and use Nginx to combine
13
+ the results into a single page.
14
+
15
+
16
+
17
+ Client Startup
18
+ --------------
19
+
20
+ ruby client.rb 1.2.3.4:9876
21
+
22
+
23
+
24
+ Server Startup
25
+ --------------
26
+
27
+ nginx -c /path/to/distributed_demo/nginx/nginx.conf
28
+ ruby server.rb 9876
29
+
30
+
31
+
32
+ Cheers,
33
+
34
+ Tyler
data/lib/array.rb ADDED
@@ -0,0 +1,5 @@
1
+ class Array
2
+ def rand
3
+ self[Kernel.rand * self.size]
4
+ end
5
+ end
data/lib/client.rb ADDED
@@ -0,0 +1,64 @@
1
+ $: << './lib'
2
+
3
+ require 'dist'
4
+ require 'encoding'
5
+ require 'messages'
6
+ require 'rubygems'
7
+ require 'rack'
8
+
9
+ module Dist
10
+ class Client
11
+ include Encoding
12
+
13
+ def initialize(address)
14
+ leave_a_will
15
+
16
+ host, port = address.split(':')
17
+ @socket = TCPSocket.new(host, port.to_i)
18
+
19
+ @socket << Messages::REQUEST_ROLE
20
+
21
+ receive_message Messages::SEND_ROLE_CONFIRM
22
+ role = receive_string
23
+
24
+ puts "Your role: #{role.dump}"
25
+
26
+ @socket << Messages::CONFIRM_ROLE
27
+ send_string @socket, role
28
+
29
+ # spin up rack app
30
+ require "roles/#{role}/app"
31
+ Thread.new { Rack::Handler::Mongrel.run App.new, :Port => 9501 }
32
+
33
+ while true
34
+ receive_message Messages::REQUEST_HEARTBEAT
35
+ @socket << Messages::HEARTBEAT
36
+ end
37
+ rescue UnexpectedMessage
38
+ puts 'Disconnected.'
39
+ end
40
+
41
+ private
42
+
43
+ def leave_a_will
44
+ graceful_death = lambda { @socket.close }
45
+ %w[INT TERM].each { |code| Signal.trap(code, &graceful_death) }
46
+ end
47
+
48
+ def receive_message(expected_message=nil)
49
+ message = @socket.recv(1)
50
+ raise UnexpectedMessage if expected_message && expected_message != message
51
+ end
52
+
53
+ def receive_string
54
+ length = @socket.recv(2).unpack('n')[0]
55
+ return '' if !length || length < 1
56
+ return @socket.recv(length)
57
+ end
58
+ end
59
+ end
60
+
61
+ if __FILE__ == $0
62
+ Thread.abort_on_exception = true
63
+ Dist::Client.new(ARGV[0])
64
+ end
data/lib/dist.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'socket'
2
+
3
+ module Dist
4
+ class UnexpectedMessage < StandardError ; end
5
+ end
data/lib/encoding.rb ADDED
@@ -0,0 +1,8 @@
1
+ module Dist
2
+ module Encoding
3
+ def send_string(out, string)
4
+ out << [string.size].pack('n')
5
+ out << string
6
+ end
7
+ end
8
+ end
data/lib/master_app.rb ADDED
@@ -0,0 +1,40 @@
1
+ require 'erb'
2
+ require 'array'
3
+ require 'sinatra/base'
4
+
5
+ class MasterApp < Sinatra::Base
6
+ def initialize(demo_server)
7
+ @demo_server = demo_server
8
+ super(*[])
9
+ end
10
+
11
+ get '/disconnect' do
12
+ @demo_server.disconnect_ip params[:ip]
13
+ redirect '/status'
14
+ end
15
+
16
+ get '/status' do
17
+ erb :status
18
+ end
19
+
20
+ get '/debug' do
21
+ debugger
22
+ end
23
+
24
+ get '/' do
25
+ @roles = @demo_server.roles.inject({}) { |o,(role,nodes)|
26
+ node = nil
27
+ while !node && nodes.size > 0
28
+ node = nodes.to_a.rand
29
+ if node.closed?
30
+ nodes.delete(node)
31
+ node = nil
32
+ end
33
+ end
34
+ o[role] = node && node.remote_ip
35
+ o
36
+ }
37
+
38
+ erb :master
39
+ end
40
+ end
data/lib/messages.rb ADDED
@@ -0,0 +1,9 @@
1
+ module Dist
2
+ module Messages
3
+ REQUEST_ROLE = 1.chr
4
+ SEND_ROLE_CONFIRM = 2.chr
5
+ CONFIRM_ROLE = 3.chr
6
+ REQUEST_HEARTBEAT = 4.chr
7
+ HEARTBEAT = 5.chr
8
+ end
9
+ end
data/lib/server.rb ADDED
@@ -0,0 +1,113 @@
1
+ $: << './lib'
2
+
3
+ require 'rubygems'
4
+ require 'dist'
5
+ require 'encoding'
6
+ require 'messages'
7
+ require 'set'
8
+ require 'rack'
9
+ require 'master_app'
10
+ require 'tcp_socket'
11
+
12
+ module Dist
13
+ class Server
14
+ include Encoding
15
+
16
+ def initialize(port)
17
+ @server = TCPServer.new(port)
18
+ @roles = Dir[File.join(File.dirname(__FILE__), 'roles', '*')].map { |p| p.split('/').last }.inject({}) { |o,f| o[f] = Set.new; o }
19
+ end
20
+
21
+ attr_reader :roles
22
+
23
+ def disconnect_ip(ip)
24
+ @roles.each do |role,sockets|
25
+ sockets.select { |sock| sock.remote_ip == ip }.each do |sock|
26
+ sockets.delete(sock)
27
+ sock.close
28
+ end
29
+ end
30
+ end
31
+
32
+ def listen
33
+ puts 'Listening...'
34
+
35
+ while (tcp_session = @server.accept)
36
+
37
+ remote_ip = tcp_session.remote_ip
38
+ disconnect_ip remote_ip
39
+
40
+ Thread.new do
41
+ puts "Client connected: #{tcp_session.peeraddr.inspect}"
42
+
43
+ role = next_role
44
+
45
+ begin
46
+ receive_message tcp_session, Messages::REQUEST_ROLE
47
+
48
+ tcp_session << Messages::SEND_ROLE_CONFIRM
49
+ send_string tcp_session, role
50
+
51
+ receive_message tcp_session, Messages::CONFIRM_ROLE
52
+ confirmed_role = receive_string(tcp_session)
53
+
54
+ if confirmed_role == role
55
+ puts 'Role confirmation complete. Putting client into production.'
56
+ @roles[role] << tcp_session
57
+ else
58
+ puts 'Role client sent back did not match role server sent... disconnecting client.'
59
+ tcp_session.close rescue nil
60
+ end
61
+ rescue UnexpectedMessage
62
+ puts 'Unexpected message... disconnecting client.'
63
+ tcp_session.close rescue nil
64
+ end
65
+
66
+ p @roles
67
+
68
+ begin
69
+ while true
70
+ sleep 3
71
+ tcp_session << Messages::REQUEST_HEARTBEAT
72
+ if tcp_session.ready_to_read?(5)
73
+ receive_message tcp_session, Messages::HEARTBEAT
74
+ #puts "#{remote_ip} - #{role}: Heartbeat"
75
+ else
76
+ puts "#{remote_ip} - #{role}: Timeout"
77
+ break
78
+ end
79
+ end
80
+ rescue => e
81
+ puts "#{remote_ip} - #{role}: #{e.message}"
82
+ puts "#{remote_ip} - #{role}: Dead"
83
+ ensure
84
+ tcp_session.close rescue nil
85
+ @roles[role].delete(tcp_session)
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ def next_role
92
+ @roles.keys.rand.min { |a,b| @roles[a].size <=> @roles[b].size }
93
+ end
94
+
95
+ def receive_message(session, expected_message=nil)
96
+ message = session.recvfrom(1).first
97
+ raise UnexpectedMessage if expected_message && expected_message != message
98
+ end
99
+
100
+ def receive_string(session)
101
+ length = session.recvfrom(2).first.unpack('n')[0]
102
+ return '' if length < 1
103
+ return session.recvfrom(length).first
104
+ end
105
+ end
106
+ end
107
+
108
+ if __FILE__ == $0
109
+ Thread.abort_on_exception = true
110
+ server = Dist::Server.new(ARGV[0].to_i)
111
+ Thread.new { server.listen }
112
+ Rack::Handler::Thin.run MasterApp.new(server), :Port => 9505
113
+ end
data/lib/tcp_socket.rb ADDED
@@ -0,0 +1,10 @@
1
+ class TCPSocket
2
+ def remote_ip
3
+ peeraddr.last.split(':').last
4
+ end
5
+
6
+ def ready_to_read?(timeout=5)
7
+ r,_,__ = IO.select([self], nil, nil, timeout)
8
+ return r.first == self
9
+ end
10
+ end
data/nginx/mime.types ADDED
@@ -0,0 +1,73 @@
1
+
2
+ types {
3
+ text/html html htm shtml;
4
+ text/css css;
5
+ text/xml xml;
6
+ image/gif gif;
7
+ image/jpeg jpeg jpg;
8
+ application/x-javascript js;
9
+ application/atom+xml atom;
10
+ application/rss+xml rss;
11
+
12
+ text/mathml mml;
13
+ text/plain txt;
14
+ text/vnd.sun.j2me.app-descriptor jad;
15
+ text/vnd.wap.wml wml;
16
+ text/x-component htc;
17
+
18
+ image/png png;
19
+ image/tiff tif tiff;
20
+ image/vnd.wap.wbmp wbmp;
21
+ image/x-icon ico;
22
+ image/x-jng jng;
23
+ image/x-ms-bmp bmp;
24
+ image/svg+xml svg;
25
+
26
+ application/java-archive jar war ear;
27
+ application/mac-binhex40 hqx;
28
+ application/msword doc;
29
+ application/pdf pdf;
30
+ application/postscript ps eps ai;
31
+ application/rtf rtf;
32
+ application/vnd.ms-excel xls;
33
+ application/vnd.ms-powerpoint ppt;
34
+ application/vnd.wap.wmlc wmlc;
35
+ application/vnd.wap.xhtml+xml xhtml;
36
+ application/vnd.google-earth.kml+xml kml;
37
+ application/vnd.google-earth.kmz kmz;
38
+ application/x-cocoa cco;
39
+ application/x-java-archive-diff jardiff;
40
+ application/x-java-jnlp-file jnlp;
41
+ application/x-makeself run;
42
+ application/x-perl pl pm;
43
+ application/x-pilot prc pdb;
44
+ application/x-rar-compressed rar;
45
+ application/x-redhat-package-manager rpm;
46
+ application/x-sea sea;
47
+ application/x-shockwave-flash swf;
48
+ application/x-stuffit sit;
49
+ application/x-tcl tcl tk;
50
+ application/x-x509-ca-cert der pem crt;
51
+ application/x-xpinstall xpi;
52
+ application/zip zip;
53
+
54
+ application/octet-stream bin exe dll;
55
+ application/octet-stream deb;
56
+ application/octet-stream dmg;
57
+ application/octet-stream eot;
58
+ application/octet-stream iso img;
59
+ application/octet-stream msi msp msm;
60
+
61
+ audio/midi mid midi kar;
62
+ audio/mpeg mp3;
63
+ audio/x-realaudio ra;
64
+
65
+ video/3gpp 3gpp 3gp;
66
+ video/mpeg mpeg mpg;
67
+ video/quicktime mov;
68
+ video/x-flv flv;
69
+ video/x-mng mng;
70
+ video/x-ms-asf asx asf;
71
+ video/x-ms-wmv wmv;
72
+ video/x-msvideo avi;
73
+ }
data/nginx/nginx.conf ADDED
@@ -0,0 +1,36 @@
1
+ worker_processes 1;
2
+
3
+ events {
4
+ worker_connections 1024;
5
+ }
6
+
7
+ http {
8
+ include mime.types;
9
+ default_type text/html;
10
+
11
+ sendfile on;
12
+ keepalive_timeout 65;
13
+
14
+ resolver 4.2.2.2;
15
+
16
+ error_log "/Users/tyler/Code/dist-demo/logs/error.log" debug;
17
+
18
+ server {
19
+ listen 9510;
20
+ server_name 127.0.0.1;
21
+
22
+ root /Users/tyler/Code/dist-demo;
23
+
24
+ location / {
25
+ ssi on;
26
+ proxy_pass http://127.0.0.1:9505/;
27
+ }
28
+
29
+ location ~* /remote/(.*)$ {
30
+ proxy_connect_timeout 2;
31
+ proxy_read_timeout 2;
32
+ proxy_send_timeout 1;
33
+ proxy_pass http://$1/;
34
+ }
35
+ }
36
+ }
data/views/master.erb ADDED
@@ -0,0 +1,63 @@
1
+ <html>
2
+ <head>
3
+ <style type="text/css">
4
+ body {
5
+ width: 900px;
6
+ }
7
+
8
+ #content {
9
+ width: 600px;
10
+ min-height: 500px;
11
+ float: left;
12
+ border: 5px solid blue;
13
+ }
14
+
15
+ #search, #login_box, #new_users {
16
+ width: 250px;
17
+ float: right;
18
+ margin: 10px 0;
19
+ }
20
+
21
+ #search {
22
+ border: 5px solid orange;
23
+ }
24
+ #login_box {
25
+ border: 5px solid green;
26
+ }
27
+ #new_users {
28
+ border: 5px solid purple;
29
+ }
30
+ </style>
31
+ </head>
32
+
33
+ <body>
34
+ <div id="content">
35
+ <% if @roles['content'] %>
36
+ <!--# block name="content" --><!--# endblock -->
37
+ <!--# include virtual="/remote/<%= @roles['content'] %>:9501" stub="content" -->
38
+ <% end %>
39
+ </div>
40
+
41
+ <div id="search">
42
+ <% if @roles['search'] %>
43
+ <!--# block name="search" --><!--# endblock -->
44
+ <!--# include virtual="/remote/<%= @roles['search'] %>:9501" stub="search" -->
45
+ <% end %>
46
+ </div>
47
+
48
+ <div id="login_box">
49
+ <% if @roles['login_box'] %>
50
+ <!--# block name="login_box" --><!--# endblock -->
51
+ <!--# include virtual="/remote/<%= @roles['login_box'] %>:9501" stub="login_box" -->
52
+ <% end %>
53
+ </div>
54
+
55
+ <div id="new_users">
56
+ <% if @roles['new_users'] %>
57
+ <!--# block name="new_users" --><!--# endblock -->
58
+ <!--# include virtual="/remote/<%= @roles['new_users'] %>:9501" stub="new_users" -->
59
+ <% end %>
60
+ </div>
61
+
62
+ </body>
63
+ </html>
data/views/status.erb ADDED
@@ -0,0 +1,23 @@
1
+ <html>
2
+ <head>
3
+ <style type="text/css">
4
+ </style>
5
+ </head>
6
+
7
+ <body>
8
+ <h1>Connection Status</h1>
9
+
10
+ <% @demo_server.roles.each do |role,clients| %>
11
+
12
+ <h2><%= role %></h2>
13
+ <ul>
14
+ <% clients.each do |client| %>
15
+ <li>
16
+ <%= client.remote_ip %>
17
+ (<a href="/disconnect?ip=<%= client.remote_ip %>">disconnect</a>)
18
+ </li>
19
+ <% end %>
20
+ </ul>
21
+ <% end %>
22
+ </body>
23
+ </html>
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 1
7
7
  - 0
8
- - 4
9
- version: 1.0.4
8
+ - 5
9
+ version: 1.0.5
10
10
  platform: ruby
11
11
  authors:
12
12
  - Tyler McMullen
@@ -65,6 +65,19 @@ extra_rdoc_files: []
65
65
  files:
66
66
  - bin/distributed_demo
67
67
  - bin/distributed_demo_server
68
+ - lib/array.rb
69
+ - lib/client.rb
70
+ - lib/dist.rb
71
+ - lib/encoding.rb
72
+ - lib/master_app.rb
73
+ - lib/messages.rb
74
+ - lib/server.rb
75
+ - lib/tcp_socket.rb
76
+ - views/master.erb
77
+ - views/status.erb
78
+ - nginx/mime.types
79
+ - nginx/nginx.conf
80
+ - README
68
81
  has_rdoc: true
69
82
  homepage:
70
83
  licenses: []