distributed_demo 1.0.4 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
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: []